Commit 5cbdbdf3 authored by Nick Thomas's avatar Nick Thomas

Merge remote-tracking branch 'upstream/master' into ce-ee-upstream

parents fb3f326d 12088309
......@@ -252,6 +252,10 @@ Layout/Tab:
Layout/TrailingBlankLines:
Enabled: true
# Avoid trailing whitespace.
Layout/TrailingWhitespace:
Enabled: true
# Style #######################################################################
# Check the naming of accessor methods for get_/set_.
......@@ -1179,29 +1183,33 @@ RSpec/VerifiedDoubles:
GitlabSecurity/DeepMunge:
Enabled: true
Exclude:
- 'spec/**/*'
- 'lib/**/*.rake'
- 'spec/**/*'
GitlabSecurity/PublicSend:
Enabled: true
Exclude:
- 'spec/**/*'
- 'config/**/*'
- 'db/**/*'
- 'features/**/*'
- 'lib/**/*.rake'
- 'qa/**/*'
- 'spec/**/*'
GitlabSecurity/RedirectToParamsUpdate:
Enabled: true
Exclude:
- 'spec/**/*'
- 'lib/**/*.rake'
- 'spec/**/*'
GitlabSecurity/SqlInjection:
Enabled: true
Exclude:
- 'spec/**/*'
- 'lib/**/*.rake'
- 'spec/**/*'
GitlabSecurity/SystemCommandInjection:
Enabled: true
Exclude:
- 'spec/**/*'
- 'lib/**/*.rake'
- 'spec/**/*'
......@@ -57,11 +57,6 @@ Layout/SpaceInsideParens:
Layout/SpaceInsidePercentLiteralDelimiters:
Enabled: false
# Offense count: 89
# Cop supports --auto-correct.
Layout/TrailingWhitespace:
Enabled: false
# Offense count: 272
RSpec/EmptyLineAfterFinalLet:
Enabled: false
......
......@@ -154,8 +154,6 @@ end
# State machine
gem 'state_machines-activerecord', '~> 0.4.0'
# Run events after state machine commits
gem 'after_commit_queue', '~> 1.3.0'
# Issue tags
gem 'acts-as-taggable-on', '~> 4.0'
......
......@@ -46,8 +46,6 @@ GEM
ice_nine (~> 0.11.0)
memoizable (~> 0.4.0)
addressable (2.3.8)
after_commit_queue (1.3.0)
activerecord (>= 3.0)
akismet (2.0.0)
allocations (1.0.5)
arel (6.0.4)
......@@ -993,7 +991,6 @@ DEPENDENCIES
activerecord_sane_schema_dumper (= 0.2)
acts-as-taggable-on (~> 4.0)
addressable (~> 2.3.8)
after_commit_queue (~> 1.3.0)
akismet (~> 2.0)
allocations (~> 1.0)
asana (~> 0.6.0)
......
......@@ -98,7 +98,6 @@ const Api = {
},
commitMultiple(id, data, callback) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const url = Api.buildUrl(Api.commitPath)
.replace(':id', id);
return $.ajax({
......
......@@ -43,6 +43,10 @@ $(() => {
$components.each(function () {
const $this = $(this);
const noteId = $this.attr(':note-id');
const discussionId = $this.attr(':discussion-id');
if ($this.is('comment-and-resolve-btn') && !discussionId) return;
const tmp = Vue.extend({
template: $this.get(0).outerHTML
});
......
......@@ -722,7 +722,7 @@ import initGroupAnalytics from './init_group_analytics';
return Dispatcher;
})();
$(function() {
$(window).on('load', function() {
new Dispatcher();
});
}).call(window);
......@@ -132,8 +132,9 @@ import './project_select';
import './project_show';
import './project_variables';
import './projects_list';
import './render_gfm';
import './syntax_highlight';
import './render_math';
import './render_gfm';
import './right_sidebar';
import './search';
import './search_autocomplete';
......@@ -141,7 +142,6 @@ import './smart_interval';
import './star';
import './subscription';
import './subscription_select';
import './syntax_highlight';
// EE-only scripts
import './admin_email_select';
......
......@@ -154,7 +154,7 @@ export default class MirrorPull {
// Show SSH public key container and fill in public key
this.toggleAuthWell(selectedAuthType);
this.toggleSSHAuthWellMessage(true);
$sshPublicKey.text(res.import_data_attributes.ssh_public_key);
this.setSSHPublicKey(res.import_data_attributes.ssh_public_key);
})
.fail(() => {
Flash('Something went wrong on our end.');
......@@ -205,4 +205,12 @@ export default class MirrorPull {
this.$wellSSHAuth.find('.js-btn-regenerate-ssh-key').toggleClass('hidden', !sshKeyPresent);
this.$wellSSHAuth.find('.js-ssh-public-key-pending').toggleClass('hidden', sshKeyPresent);
}
/**
* Sets SSH Public key to Clipboard button and shows it on UI.
*/
setSSHPublicKey(sshPublicKey) {
this.$sshPublicKeyWrap.find('.ssh-public-key').text(sshPublicKey);
this.$sshPublicKeyWrap.find('.btn-copy-ssh-public-key').attr('data-clipboard-text', sshPublicKey);
}
}
......@@ -138,11 +138,11 @@ import Cookies from 'js-cookie';
var $form = $dropdown.closest('form');
var $visit = $dropdown.data('visit');
var shouldVisit = typeof $visit === 'undefined' ? true : $visit;
var shouldVisit = $visit ? true : $visit;
var action = $form.attr('action');
var divider = action.indexOf('?') === -1 ? '?' : '&';
if (shouldVisit) {
gl.utils.visitUrl(action + '' + divider + '' + $form.serialize());
gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`);
}
}
}
......
......@@ -11,7 +11,5 @@
return this;
};
$(document).on('ready load', function() {
return $('body').renderGFM();
});
$(() => $('body').renderGFM());
}).call(window);
......@@ -14,13 +14,13 @@ export default {
data: () => Store,
mixins: [RepoMixin],
components: {
'repo-sidebar': RepoSidebar,
'repo-tabs': RepoTabs,
'repo-file-buttons': RepoFileButtons,
RepoSidebar,
RepoTabs,
RepoFileButtons,
'repo-editor': MonacoLoaderHelper.repoEditorLoader,
'repo-commit-section': RepoCommitSection,
'popup-dialog': PopupDialog,
'repo-preview': RepoPreview,
RepoCommitSection,
PopupDialog,
RepoPreview,
},
mounted() {
......@@ -28,12 +28,12 @@ export default {
},
methods: {
dialogToggled(toggle) {
toggleDialogOpen(toggle) {
this.dialog.open = toggle;
},
dialogSubmitted(status) {
this.dialog.open = false;
this.toggleDialogOpen(false);
this.dialog.status = status;
},
......@@ -43,21 +43,28 @@ export default {
</script>
<template>
<div class="repository-view tree-content-holder">
<repo-sidebar/><div class="panel-right" :class="{'edit-mode': editMode}">
<div class="repository-view">
<div class="tree-content-holder" :class="{'tree-content-holder-mini' : isMini}">
<repo-sidebar/>
<div v-if="isMini"
class="panel-right"
:class="{'edit-mode': editMode}">
<repo-tabs/>
<component :is="currentBlobView" class="blob-viewer-container"></component>
<component
:is="currentBlobView"
class="blob-viewer-container"/>
<repo-file-buttons/>
</div>
</div>
<repo-commit-section/>
<popup-dialog
v-show="dialog.open"
:primary-button-label="__('Discard changes')"
:open="dialog.open"
kind="warning"
:title="__('Are you sure?')"
:body="__('Are you sure you want to discard your changes?')"
@toggle="dialogToggled"
@toggle="toggleDialogOpen"
@submit="dialogSubmitted"
/>
</div>
</div>
</template>
......@@ -2,18 +2,20 @@
/* global Flash */
import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin';
import Helper from '../helpers/repo_helper';
import Service from '../services/repo_service';
const RepoCommitSection = {
export default {
data: () => Store,
mixins: [RepoMixin],
computed: {
showCommitable() {
return this.isCommitable && this.changedFiles.length;
},
branchPaths() {
const branch = Helper.getBranch();
return this.changedFiles.map(f => Helper.getFilePathFromFullPath(f.url, branch));
return this.changedFiles.map(f => f.path);
},
cantCommitYet() {
......@@ -28,11 +30,10 @@ const RepoCommitSection = {
methods: {
makeCommit() {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const branch = Helper.getBranch();
const commitMessage = this.commitMessage;
const actions = this.changedFiles.map(f => ({
action: 'update',
file_path: Helper.getFilePathFromFullPath(f.url, branch),
file_path: f.path,
content: f.newContent,
}));
const payload = {
......@@ -47,51 +48,80 @@ const RepoCommitSection = {
resetCommitState() {
this.submitCommitsLoading = false;
this.changedFiles = [];
this.openedFiles = [];
this.commitMessage = '';
this.editMode = false;
$('html, body').animate({ scrollTop: 0 }, 'fast');
window.scrollTo(0, 0);
},
},
};
export default RepoCommitSection;
</script>
<template>
<div id="commit-area" v-if="isCommitable && changedFiles.length" >
<form class="form-horizontal">
<div
v-if="showCommitable"
id="commit-area">
<form
class="form-horizontal"
@submit.prevent="makeCommit">
<fieldset>
<div class="form-group">
<label class="col-md-4 control-label staged-files">Staged files ({{changedFiles.length}})</label>
<div class="col-md-4">
<label class="col-md-4 control-label staged-files">
Staged files ({{changedFiles.length}})
</label>
<div class="col-md-6">
<ul class="list-unstyled changed-files">
<li v-for="file in branchPaths" :key="file.id">
<span class="help-block">{{file}}</span>
<li
v-for="branchPath in branchPaths"
:key="branchPath">
<span class="help-block">
{{branchPath}}
</span>
</li>
</ul>
</div>
</div>
<!-- Textarea
-->
<div class="form-group">
<label class="col-md-4 control-label" for="commit-message">Commit message</label>
<div class="col-md-4">
<textarea class="form-control" id="commit-message" name="commit-message" v-model="commitMessage"></textarea>
<label
class="col-md-4 control-label"
for="commit-message">
Commit message
</label>
<div class="col-md-6">
<textarea
id="commit-message"
class="form-control"
name="commit-message"
v-model="commitMessage">
</textarea>
</div>
</div>
<!-- Button Drop Down
-->
<div class="form-group target-branch">
<label class="col-md-4 control-label" for="target-branch">Target branch</label>
<div class="col-md-4">
<span class="help-block">{{targetBranch}}</span>
<label
class="col-md-4 control-label"
for="target-branch">
Target branch
</label>
<div class="col-md-6">
<span class="help-block">
{{targetBranch}}
</span>
</div>
</div>
<div class="col-md-offset-4 col-md-4">
<button type="submit" :disabled="cantCommitYet" class="btn btn-success submit-commit" @click.prevent="makeCommit">
<i class="fa fa-spinner fa-spin" v-if="submitCommitsLoading"></i>
<span class="commit-summary">Commit {{changedFiles.length}} {{filePluralize}}</span>
<div class="col-md-offset-4 col-md-6">
<button
ref="submitCommit"
type="submit"
:disabled="cantCommitYet"
class="btn btn-success">
<i
v-if="submitCommitsLoading"
class="fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="loading">
</i>
<span class="commit-summary">
Commit {{changedFiles.length}} {{filePluralize}}
</span>
</button>
</div>
</fieldset>
......
......@@ -10,12 +10,15 @@ export default {
return this.editMode ? this.__('Cancel edit') : this.__('Edit');
},
buttonIcon() {
return this.editMode ? [] : ['fa', 'fa-pencil'];
showButton() {
return this.isCommitable &&
!this.activeFile.render_error &&
!this.binary &&
this.openedFiles.length;
},
},
methods: {
editClicked() {
editCancelClicked() {
if (this.changedFiles.length) {
this.dialog.open = true;
return;
......@@ -23,10 +26,15 @@ export default {
this.editMode = !this.editMode;
Store.toggleBlobView();
},
toggleProjectRefsForm() {
$('.project-refs-form').toggleClass('disabled', this.editMode);
$('.js-tree-ref-target-holder').toggle(this.editMode);
},
},
watch: {
editMode() {
<<<<<<< HEAD
if (this.editMode) {
$('.project-refs-form').addClass('disabled');
$('.js-tree-ref-target-holder').show();
......@@ -34,14 +42,27 @@ export default {
$('.project-refs-form').removeClass('disabled');
$('.js-tree-ref-target-holder').hide();
}
=======
this.toggleProjectRefsForm();
>>>>>>> upstream/master
},
},
};
</script>
<template>
<button class="btn btn-default" @click.prevent="editClicked" v-cloak v-if="isCommitable && !activeFile.render_error" :disabled="binary">
<i :class="buttonIcon"></i>
<span>{{buttonLabel}}</span>
<button
v-if="showButton"
class="btn btn-default"
type="button"
@click.prevent="editCancelClicked">
<i
v-if="!editMode"
class="fa fa-pencil"
aria-hidden="true">
</i>
<span>
{{buttonLabel}}
</span>
</button>
</template>
......@@ -8,38 +8,39 @@ const RepoEditor = {
data: () => Store,
destroyed() {
// this.monacoInstance.getModels().forEach((m) => {
// m.dispose();
// });
this.monacoInstance.destroy();
if (Helper.monacoInstance) {
Helper.monacoInstance.destroy();
}
},
mounted() {
Service.getRaw(this.activeFile.raw_path)
.then((rawResponse) => {
Store.blobRaw = rawResponse.data;
Helper.findOpenedFileFromActive().plain = rawResponse.data;
Store.activeFile.plain = rawResponse.data;
const monacoInstance = this.monaco.editor.create(this.$el, {
const monacoInstance = Helper.monaco.editor.create(this.$el, {
model: null,
readOnly: false,
contextmenu: false,
});
Store.monacoInstance = monacoInstance;
Helper.monacoInstance = monacoInstance;
this.addMonacoEvents();
const languages = this.monaco.languages.getLanguages();
const languageID = Helper.getLanguageIDForFile(this.activeFile, languages);
this.setupEditor();
})
.catch(Helper.loadingError);
},
methods: {
setupEditor() {
this.showHide();
const newModel = this.monaco.editor.createModel(this.blobRaw, languageID);
this.monacoInstance.setModel(newModel);
}).catch(Helper.loadingError);
Helper.setMonacoModelFromLanguage();
},
methods: {
showHide() {
if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) {
this.$el.style.display = 'none';
......@@ -49,41 +50,36 @@ const RepoEditor = {
},
addMonacoEvents() {
this.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp);
this.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this));
Helper.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp);
Helper.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this));
},
onMonacoEditorKeysPressed() {
Store.setActiveFileContents(this.monacoInstance.getValue());
Store.setActiveFileContents(Helper.monacoInstance.getValue());
},
onMonacoEditorMouseUp(e) {
if (!e.target.position) return;
const lineNumber = e.target.position.lineNumber;
if (e.target.element.className === 'line-numbers') {
if (e.target.element.classList.contains('line-numbers')) {
location.hash = `L${lineNumber}`;
Store.activeLine = lineNumber;
}
},
},
watch: {
activeLine() {
this.monacoInstance.setPosition({
Helper.monacoInstance.setPosition({
lineNumber: this.activeLine,
column: 1,
});
}
},
activeFileLabel() {
this.showHide();
},
watch: {
dialog: {
handler(obj) {
const newObj = obj;
if (newObj.status) {
newObj.status = false;
this.openedFiles.map((file) => {
this.openedFiles = this.openedFiles.map((file) => {
const f = file;
if (f.active) {
this.blobRaw = f.plain;
......@@ -94,35 +90,21 @@ const RepoEditor = {
return f;
});
this.editMode = false;
Store.toggleBlobView();
}
},
deep: true,
},
isTree() {
this.showHide();
},
openedFiles() {
this.showHide();
blobRaw() {
if (Helper.monacoInstance && !this.isTree) {
this.setupEditor();
}
},
binary() {
this.showHide();
},
blobRaw() {
this.showHide();
if (this.isTree) return;
this.monacoInstance.setModel(null);
const languages = this.monaco.languages.getLanguages();
const languageID = Helper.getLanguageIDForFile(this.activeFile, languages);
const newModel = this.monaco.editor.createModel(this.blobRaw, languageID);
this.monacoInstance.setModel(newModel);
computed: {
shouldHideEditor() {
return !this.openedFiles.length || (this.binary && !this.activeFile.raw);
},
},
};
......@@ -131,5 +113,5 @@ export default RepoEditor;
</script>
<template>
<div id="ide"></div>
<div id="ide" v-if='!shouldHideEditor'></div>
</template>
......@@ -33,6 +33,26 @@ const RepoFile = {
canShowFile() {
return !this.loading.tree || this.hasFiles;
},
fileIcon() {
const classObj = {
'fa-spinner fa-spin': this.file.loading,
[this.file.icon]: !this.file.loading,
};
return classObj;
},
fileIndentation() {
return {
'margin-left': `${this.file.level * 10}px`,
};
},
activeFileClass() {
return {
active: this.activeFile.url === this.file.url,
};
},
},
methods: {
......@@ -46,21 +66,42 @@ export default RepoFile;
</script>
<template>
<tr class="file" v-if="canShowFile" :class="{'active': activeFile.url === file.url}">
<td @click.prevent="linkClicked(file)">
<i class="fa file-icon" v-if="!file.loading" :class="file.icon" :style="{'margin-left': file.level * 10 + 'px'}"></i>
<i class="fa fa-spinner fa-spin" v-if="file.loading" :style="{'margin-left': file.level * 10 + 'px'}"></i>
<a :href="file.url" class="repo-file-name" :title="file.url">{{file.name}}</a>
<tr
v-if="canShowFile"
class="file"
:class="activeFileClass"
@click.prevent="linkClicked(file)">
<td>
<i
class="fa fa-fw file-icon"
:class="fileIcon"
:style="fileIndentation"
aria-label="file icon">
</i>
<a
:href="file.url"
class="repo-file-name"
:title="file.url">
{{file.name}}
</a>
</td>
<td v-if="!isMini" class="hidden-sm hidden-xs">
<template v-if="!isMini">
<td class="hidden-sm hidden-xs">
<div class="commit-message">
<a :href="file.lastCommitUrl">{{file.lastCommitMessage}}</a>
<a @click.stop :href="file.lastCommitUrl">
{{file.lastCommitMessage}}
</a>
</div>
</td>
<td v-if="!isMini" class="hidden-xs">
<span class="commit-update" :title="tooltipTitle(file.lastCommitUpdate)">{{timeFormated(file.lastCommitUpdate)}}</span>
<td class="hidden-xs">
<span
class="commit-update"
:title="tooltipTitle(file.lastCommitUpdate)">
{{timeFormated(file.lastCommitUpdate)}}
</span>
</td>
</template>
</tr>
</template>
......@@ -15,7 +15,7 @@ const RepoFileButtons = {
},
canPreview() {
return Helper.isKindaBinary();
return Helper.isRenderable();
},
},
......@@ -28,15 +28,42 @@ export default RepoFileButtons;
</script>
<template>
<div id="repo-file-buttons" v-if="isMini">
<a :href="activeFile.raw_path" target="_blank" class="btn btn-default raw" rel="noopener noreferrer">{{rawDownloadButtonLabel}}</a>
<div id="repo-file-buttons">
<a
:href="activeFile.raw_path"
target="_blank"
class="btn btn-default raw"
rel="noopener noreferrer">
{{rawDownloadButtonLabel}}
</a>
<div class="btn-group" role="group" aria-label="File actions">
<a :href="activeFile.blame_path" class="btn btn-default blame">Blame</a>
<a :href="activeFile.commits_path" class="btn btn-default history">History</a>
<a :href="activeFile.permalink" class="btn btn-default permalink">Permalink</a>
<div
class="btn-group"
role="group"
aria-label="File actions">
<a
:href="activeFile.blame_path"
class="btn btn-default blame">
Blame
</a>
<a
:href="activeFile.commits_path"
class="btn btn-default history">
History
</a>
<a
:href="activeFile.permalink"
class="btn btn-default permalink">
Permalink
</a>
</div>
<a href="#" v-if="canPreview" @click.prevent="rawPreviewToggle" class="btn btn-default preview">{{activeFileLabel}}</a>
</div>
<a
v-if="canPreview"
href="#"
@click.prevent="rawPreviewToggle"
class="btn btn-default preview">
{{activeFileLabel}}
</a>
</div>
</template>
......@@ -17,7 +17,7 @@ export default RepoFileOptions;
</script>
<template>
<tr v-if="isMini" class="repo-file-options">
<tr v-if="isMini" class="repo-file-options">
<td>
<span class="title">{{projectName}}</span>
</td>
......
......@@ -18,9 +18,15 @@ const RepoLoadingFile = {
},
},
computed: {
showGhostLines() {
return this.loading.tree && !this.hasFiles;
},
},
methods: {
lineOfCode(n) {
return `line-of-code-${n}`;
return `skeleton-line-${n}`;
},
},
};
......@@ -29,23 +35,42 @@ export default RepoLoadingFile;
</script>
<template>
<tr v-if="loading.tree && !hasFiles" class="loading-file">
<tr
v-if="showGhostLines"
class="loading-file">
<td>
<div class="animation-container animation-container-small">
<div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div>
<div
class="animation-container animation-container-small">
<div
v-for="n in 6"
:key="n"
:class="lineOfCode(n)">
</div>
</div>
</td>
<td v-if="!isMini" class="hidden-sm hidden-xs">
<td
v-if="!isMini"
class="hidden-sm hidden-xs">
<div class="animation-container">
<div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div>
<div
v-for="n in 6"
:key="n"
:class="lineOfCode(n)">
</div>
</div>
</td>
<td v-if="!isMini" class="hidden-xs">
<td
v-if="!isMini"
class="hidden-xs">
<div class="animation-container animation-container-small">
<div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div>
<div
v-for="n in 6"
:key="n"
:class="lineOfCode(n)">
</div>
</div>
</td>
</tr>
</tr>
</template>
<script>
import RepoMixin from '../mixins/repo_mixin';
const RepoPreviousDirectory = {
props: {
prevUrl: {
......@@ -7,6 +9,14 @@ const RepoPreviousDirectory = {
},
},
mixins: [RepoMixin],
computed: {
colSpanCondition() {
return this.isMini ? undefined : 3;
},
},
methods: {
linkClicked(file) {
this.$emit('linkclicked', file);
......@@ -19,8 +29,10 @@ export default RepoPreviousDirectory;
<template>
<tr class="prev-directory">
<td colspan="3">
<a :href="prevUrl" @click.prevent="linkClicked(prevUrl)">..</a>
<td
:colspan="colSpanCondition"
@click.prevent="linkClicked(prevUrl)">
<a :href="prevUrl">..</a>
</td>
</tr>
</template>
......@@ -8,7 +8,7 @@ import RepoFile from './repo_file.vue';
import RepoLoadingFile from './repo_loading_file.vue';
import RepoMixin from '../mixins/repo_mixin';
const RepoSidebar = {
export default {
mixins: [RepoMixin],
components: {
'repo-file-options': RepoFileOptions,
......@@ -35,7 +35,11 @@ const RepoSidebar = {
fileClicked(clickedFile) {
let file = clickedFile;
<<<<<<< HEAD
=======
if (file.loading) return;
>>>>>>> upstream/master
file.loading = true;
if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file);
......@@ -59,12 +63,10 @@ const RepoSidebar = {
},
},
};
export default RepoSidebar;
</script>
<template>
<div id="sidebar" :class="{'sidebar-mini' : isMini}" v-cloak>
<div id="sidebar" :class="{'sidebar-mini' : isMini}">
<table class="table">
<thead v-if="!isMini">
<tr>
......
......@@ -18,8 +18,8 @@ const RepoTab = {
},
changedClass() {
const tabChangedObj = {
'fa-times': !this.tab.changed,
'fa-circle': this.tab.changed,
'fa-times close-icon': !this.tab.changed,
'fa-circle unsaved-icon': this.tab.changed,
};
return tabChangedObj;
},
......@@ -28,9 +28,9 @@ const RepoTab = {
methods: {
tabClicked: Store.setActiveFiles,
xClicked(file) {
closeTab(file) {
if (file.changed) return;
this.$emit('xclicked', file);
this.$emit('tabclosed', file);
},
},
};
......@@ -39,11 +39,19 @@ export default RepoTab;
</script>
<template>
<<<<<<< HEAD
<li>
<a
href="#0"
class="close"
@click.prevent="xClicked(tab)"
=======
<li @click="tabClicked(tab)">
<a
href="#0"
class="close"
@click.stop.prevent="closeTab(tab)"
>>>>>>> upstream/master
:aria-label="closeLabel">
<i
class="fa"
......
......@@ -13,7 +13,11 @@ const RepoTabs = {
data: () => Store,
methods: {
<<<<<<< HEAD
xClicked(file) {
=======
tabClosed(file) {
>>>>>>> upstream/master
Store.removeFromOpenedFiles(file);
},
},
......@@ -23,10 +27,21 @@ export default RepoTabs;
</script>
<template>
<<<<<<< HEAD
<ul
v-if="isMini"
id="tabs">
<repo-tab v-for="tab in openedFiles" :key="tab.id" :tab="tab" :class="{'active' : tab.active}" @xclicked="xClicked"/>
=======
<ul id="tabs">
<repo-tab
v-for="tab in openedFiles"
:key="tab.id"
:tab="tab"
:class="{'active' : tab.active}"
@tabclosed="tabClosed"
/>
>>>>>>> upstream/master
<li class="tabs-divider" />
</ul>
</template>
/* global monaco */
import RepoEditor from '../components/repo_editor.vue';
import Store from '../stores/repo_store';
import Helper from '../helpers/repo_helper';
import monacoLoader from '../monaco_loader';
function repoEditorLoader() {
Store.monacoLoading = true;
return new Promise((resolve, reject) => {
monacoLoader(['vs/editor/editor.main'], () => {
Store.monaco = monaco;
Helper.monaco = monaco;
Store.monacoLoading = false;
resolve(RepoEditor);
}, () => {
......
......@@ -4,6 +4,8 @@ import Store from '../stores/repo_store';
import '../../flash';
const RepoHelper = {
monacoInstance: null,
getDefaultActiveFile() {
return {
active: true,
......@@ -35,10 +37,13 @@ const RepoHelper = {
getFileExtension(fileName) {
return fileName.split('.').pop();
<<<<<<< HEAD
},
getBranch() {
return $('button.dropdown-menu-toggle').attr('data-ref');
=======
>>>>>>> upstream/master
},
getLanguageIDForFile(file, langs) {
......@@ -48,8 +53,12 @@ const RepoHelper = {
return foundLang ? foundLang.id : 'plaintext';
},
getFilePathFromFullPath(fullPath, branch) {
return fullPath.split(`${Store.projectUrl}/blob/${branch}`)[1];
setMonacoModelFromLanguage() {
RepoHelper.monacoInstance.setModel(null);
const languages = RepoHelper.monaco.languages.getLanguages();
const languageID = RepoHelper.getLanguageIDForFile(Store.activeFile, languages);
const newModel = RepoHelper.monaco.editor.createModel(Store.blobRaw, languageID);
RepoHelper.monacoInstance.setModel(newModel);
},
findLanguage(ext, langs) {
......@@ -62,11 +71,11 @@ const RepoHelper = {
file.opened = true;
file.icon = 'fa-folder-open';
RepoHelper.toURL(file.url, file.name);
RepoHelper.updateHistoryEntry(file.url, file.name);
return file;
},
isKindaBinary() {
isRenderable() {
const okExts = ['md', 'svg'];
return okExts.indexOf(Store.activeFile.extension) > -1;
},
......@@ -80,22 +89,8 @@ const RepoHelper = {
.catch(RepoHelper.loadingError);
},
toggleFakeTab(loading, file) {
if (loading) return Store.addPlaceholderFile();
return Store.removeFromOpenedFiles(file);
},
setLoading(loading, file) {
if (Service.url.indexOf('blob') > -1) {
Store.loading.blob = loading;
return RepoHelper.toggleFakeTab(loading, file);
}
if (Service.url.indexOf('tree') > -1) Store.loading.tree = loading;
return undefined;
},
// when you open a directory you need to put the directory files under
// the directory... This will merge the list of the current directory and the new list.
getNewMergedList(inDirectory, currentList, newList) {
const newListSorted = newList.sort(this.compareFilesCaseInsensitive);
if (!inDirectory) return newListSorted;
......@@ -104,6 +99,9 @@ const RepoHelper = {
return RepoHelper.mergeNewListToOldList(newListSorted, currentList, inDirectory, indexOfFile);
},
// within the get new merged list this does the merging of the current list of files
// and the new list of files. The files are never "in" another directory they just
// appear like they are because of the margin.
mergeNewListToOldList(newList, oldList, inDirectory, indexOfFile) {
newList.reverse().forEach((newFile) => {
const fileIndex = indexOfFile + 1;
......@@ -141,11 +139,13 @@ const RepoHelper = {
getContent(treeOrFile) {
let file = treeOrFile;
// const loadingData = RepoHelper.setLoading(true);
return Service.getContent()
.then((response) => {
const data = response.data;
<<<<<<< HEAD
// RepoHelper.setLoading(false, loadingData);
=======
>>>>>>> upstream/master
Store.isTree = RepoHelper.isTree(data);
if (!Store.isTree) {
if (!file) file = data;
......@@ -246,37 +246,19 @@ const RepoHelper = {
},
dataToListOfFiles(data) {
const a = [];
// push in blobs
data.blobs.forEach((blob) => {
a.push(RepoHelper.serializeBlob(blob));
});
data.trees.forEach((tree) => {
a.push(RepoHelper.serializeTree(tree));
});
data.submodules.forEach((submodule) => {
a.push(RepoHelper.serializeSubmodule(submodule));
});
return a;
const { blobs, trees, submodules } = data;
return [
...blobs.map(blob => RepoHelper.serializeBlob(blob)),
...trees.map(tree => RepoHelper.serializeTree(tree)),
...submodules.map(submodule => RepoHelper.serializeSubmodule(submodule)),
];
},
genKey() {
return RepoHelper.Time.now().toFixed(3);
},
getStateKey() {
return RepoHelper.key;
},
setStateKey(key) {
RepoHelper.key = key;
},
toURL(url, title) {
updateHistoryEntry(url, title) {
const history = window.history;
RepoHelper.key = RepoHelper.genKey();
......@@ -293,7 +275,7 @@ const RepoHelper = {
},
loadingError() {
Flash('Unable to load the file at this time.');
Flash('Unable to load this content at this time.');
},
};
......
......@@ -33,6 +33,8 @@ function setInitialStore(data) {
Store.projectId = data.projectId;
Store.projectName = data.projectName;
Store.projectUrl = data.projectUrl;
Store.canCommit = data.canCommit;
Store.onTopOfBranch = data.onTopOfBranch;
Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref');
Store.checkIsCommitable();
}
......@@ -43,6 +45,9 @@ function initRepo(el) {
components: {
repo: Repo,
},
render(createElement) {
return createElement('repo');
},
});
}
......
......@@ -13,14 +13,6 @@ const RepoService = {
},
richExtensionRegExp: /md/,
checkCurrentBranchIsCommitable() {
const url = Store.service.refsUrl;
return axios.get(url, { params: {
ref: Store.currentBranch,
search: Store.currentBranch,
} });
},
getRaw(url) {
return axios.get(url, {
// Stop Axios from parsing a JSON file into a JS object
......@@ -75,7 +67,11 @@ const RepoService = {
commitFiles(payload, cb) {
Api.commitMultiple(Store.projectId, payload, (data) => {
if (data.short_id && data.stats) {
Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
} else {
Flash(data.message);
}
cb();
});
},
......
......@@ -5,8 +5,12 @@ import Service from '../services/repo_service';
const RepoStore = {
monaco: {},
monacoLoading: false,
monacoInstance: {},
service: '',
<<<<<<< HEAD
=======
canCommit: false,
onTopOfBranch: false,
>>>>>>> upstream/master
editMode: false,
isTree: false,
isRoot: false,
......@@ -52,14 +56,7 @@ const RepoStore = {
// mutations
checkIsCommitable() {
RepoStore.service.checkCurrentBranchIsCommitable()
.then((data) => {
// you shouldn't be able to make commits on commits or tags.
const { Branches, Commits, Tags } = data.data;
if (Branches && Branches.length) RepoStore.isCommitable = true;
if (Commits && Commits.length) RepoStore.isCommitable = false;
if (Tags && Tags.length) RepoStore.isCommitable = false;
}).catch(() => Flash('Failed to check if branch can be committed to.'));
RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit;
},
addFilesToDirectory(inDirectory, currentList, newList) {
......@@ -90,7 +87,7 @@ const RepoStore = {
}).catch(Helper.loadingError);
}
if (!file.loading) Helper.toURL(file.url, file.name);
if (!file.loading) Helper.updateHistoryEntry(file.url, file.name);
RepoStore.binary = file.binary;
},
......@@ -117,15 +114,15 @@ const RepoStore = {
removeChildFilesOfTree(tree) {
let foundTree = false;
const treeToClose = tree;
let wereDone = false;
let canStopSearching = false;
RepoStore.files = RepoStore.files.filter((file) => {
const isItTheTreeWeWant = file.url === treeToClose.url;
// if it's the next tree
if (foundTree && file.type === 'tree' && !isItTheTreeWeWant && file.level === treeToClose.level) {
wereDone = true;
canStopSearching = true;
return true;
}
if (wereDone) return true;
if (canStopSearching) return true;
if (isItTheTreeWeWant) foundTree = true;
......@@ -142,8 +139,8 @@ const RepoStore = {
if (file.type === 'tree') return;
let foundIndex;
RepoStore.openedFiles = RepoStore.openedFiles.filter((openedFile, i) => {
if (openedFile.url === file.url) foundIndex = i;
return openedFile.url !== file.url;
if (openedFile.path === file.path) foundIndex = i;
return openedFile.path !== file.path;
});
// now activate the right tab based on what you closed.
......@@ -157,36 +154,16 @@ const RepoStore = {
return;
}
if (foundIndex) {
if (foundIndex > 0) {
if (foundIndex && foundIndex > 0) {
RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]);
}
}
},
addPlaceholderFile() {
const randomURL = Helper.Time.now();
const newFakeFile = {
active: false,
binary: true,
type: 'blob',
loading: true,
mime_type: 'loading',
name: 'loading',
url: randomURL,
fake: true,
};
RepoStore.openedFiles.push(newFakeFile);
return newFakeFile;
},
addToOpenedFiles(file) {
const openFile = file;
const openedFilesAlreadyExists = RepoStore.openedFiles
.some(openedFile => openedFile.url === openFile.url);
.some(openedFile => openedFile.path === openFile.path);
if (openedFilesAlreadyExists) return;
......
<script>
const PopupDialog = {
export default {
name: 'popup-dialog',
props: {
open: Boolean,
title: String,
body: String,
title: {
type: String,
required: true,
},
body: {
type: String,
required: true,
},
kind: {
type: String,
required: false,
default: 'primary',
},
closeButtonLabel: {
type: String,
required: false,
default: 'Cancel',
},
primaryButtonLabel: {
type: String,
default: 'Save changes',
required: true,
},
},
computed: {
typeOfClass() {
const className = `btn-${this.kind}`;
const returnObj = {};
returnObj[className] = true;
return returnObj;
btnKindClass() {
return {
[`btn-${this.kind}`]: true,
};
},
},
......@@ -33,33 +39,48 @@ const PopupDialog = {
close() {
this.$emit('toggle', false);
},
yesClick() {
this.$emit('submit', true);
},
noClick() {
this.$emit('submit', false);
emitSubmit(status) {
this.$emit('submit', status);
},
},
};
export default PopupDialog;
</script>
<template>
<div class="modal popup-dialog" tabindex="-1" v-show="open" role="dialog">
<div class="modal-dialog" role="document">
<div
class="modal popup-dialog"
role="dialog"
tabindex="-1">
<div
class="modal-dialog"
role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" @click="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<button type="button"
class="close"
@click="close"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">{{this.title}}</h4>
</div>
<div class="modal-body">
<p>{{this.body}}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal" @click="noClick">{{closeButtonLabel}}</button>
<button type="button" class="btn" :class="typeOfClass" @click="yesClick">{{primaryButtonLabel}}</button>
<button
type="button"
class="btn btn-default"
@click="emitSubmit(false)">
{{closeButtonLabel}}
</button>
<button
type="button"
class="btn"
:class="btnKindClass"
@click="emitSubmit(true)">
{{primaryButtonLabel}}
</button>
</div>
</div>
</div>
......
......@@ -187,3 +187,81 @@ a {
.fade-in-full {
animation: fadeInFull $fade-in-duration 1;
}
.animation-container {
background: $repo-editor-grey;
height: 40px;
overflow: hidden;
position: relative;
&.animation-container-small {
height: 12px;
}
&::before {
animation-duration: 1s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: blockTextShine;
animation-timing-function: linear;
background-image: $repo-editor-linear-gradient;
background-repeat: no-repeat;
background-size: 800px 45px;
content: ' ';
display: block;
height: 100%;
position: relative;
}
div {
background: $white-light;
height: 6px;
left: 0;
position: absolute;
right: 0;
}
.skeleton-line-1 {
left: 0;
top: 8px;
}
.skeleton-line-2 {
left: 150px;
top: 0;
height: 10px;
}
.skeleton-line-3 {
left: 0;
top: 23px;
}
.skeleton-line-4 {
left: 0;
top: 38px;
}
.skeleton-line-5 {
left: 200px;
top: 28px;
height: 10px;
}
.skeleton-line-6 {
top: 14px;
left: 230px;
height: 10px;
}
}
@keyframes blockTextShine {
0% {
transform: translateX(-468px);
}
100% {
transform: translateX(468px);
}
}
......@@ -117,10 +117,6 @@ body {
margin-top: $header-height + $performance-bar-height;
}
[v-cloak] {
display: none;
}
.vertical-center {
min-height: 100vh;
display: flex;
......
......@@ -8,13 +8,13 @@
.is-confidential {
color: $orange-600;
background-color: $orange-50;
border-radius: 3px;
border-radius: $border-radius-default;
padding: 5px;
margin: 0 3px 0 -4px;
}
.is-not-confidential {
border-radius: 3px;
border-radius: $border-radius-default;
padding: 5px;
margin: 0 3px 0 -4px;
}
......
......@@ -707,9 +707,89 @@
}
}
#merge-request-widget-app .loading {
padding-top: 5px;
border-top: 1px solid $well-inner-border;
}
.approvals-footer {
display: flex;
.approvers-prefix,
.approvers-list {
display: flex;
align-items: center;
margin-right: 5px;
}
.unapprove-btn {
border: none;
background: transparent;
cursor: pointer;
&:hover {
color: $gl-text-color-secondary;
text-decoration: none;
}
&:focus {
outline: none;
}
}
.approver-avatar {
position: relative;
}
}
.link-to-member-avatar {
.disabled {
pointer-events: none;
cursor: default;
}
.avatar {
margin-bottom: 0;
margin-left: 7px;
display: block;
}
}
.mr-memory-usage {
p.usage-info-loading .usage-info-load-spinner {
margin-right: 10px;
font-size: 16px;
}
}
.mr-widget-icon {
font-size: 22px;
margin: 0 10px 0 0;
}
.mr-widget-code-quality {
padding-top: $gl-padding-top;
.code-quality-container {
border-top: 1px solid $gray-darker;
border-bottom: 1px solid $gray-darker;
padding: $gl-padding-top;
background-color: $gray-light;
margin: 4px -16px 0;
.mr-widget-code-quality-list {
list-style: none;
padding: 4px 36px;
margin: 0;
line-height: $code_line_height;
li.success {
color: $green-500;
}
li.failed {
color: $red-500;
}
}
}
}
.fade-enter-active,
.fade-leave-active {
transition: opacity .5s;
transition: opacity $sidebar-transition-duration;
}
.monaco-loader {
......@@ -28,11 +28,6 @@
.project-refs-form,
.project-refs-target-form {
display: inline-block;
&.disabled {
opacity: 0.5;
pointer-events: none;
}
}
.fade-enter,
......@@ -52,14 +47,26 @@
margin: 20px;
}
.repository-view.tree-content-holder {
.repository-view {
border: 1px solid $border-color;
border-radius: $border-radius-default;
color: $almost-black;
.tree-content-holder {
display: flex;
max-height: 100vh;
min-height: 300px;
}
.tree-content-holder-mini {
height: 100vh;
}
.panel-right {
display: inline-block;
display: flex;
flex-direction: column;
width: 80%;
height: 100%;
.monaco-editor.vs {
.line-numbers {
......@@ -90,16 +97,17 @@
}
.blob-viewer-container {
height: calc(100vh - 63px);
flex: 1;
overflow: auto;
}
#tabs {
flex-shrink: 0;
display: flex;
width: 100%;
padding-left: 0;
margin-bottom: 0;
display: flex;
white-space: nowrap;
width: 100%;
overflow-y: hidden;
overflow-x: auto;
......@@ -114,6 +122,7 @@
border-right: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
white-space: nowrap;
cursor: pointer;
&.remove {
animation: swipeRightDissapear ease-in 0.1s;
......@@ -133,10 +142,10 @@
a {
@include str-truncated(100px);
color: $black;
display: inline-block;
width: 100px;
text-align: center;
vertical-align: middle;
text-decoration: none;
&.close {
width: auto;
......@@ -146,15 +155,15 @@
}
}
i.fa.fa-times,
i.fa.fa-circle {
.close-icon,
.unsaved-icon {
float: right;
margin-top: 3px;
margin-left: 15px;
color: $gray-darkest;
}
i.fa.fa-circle {
.unsaved-icon {
color: $brand-success;
}
......@@ -204,7 +213,7 @@
background: $gray-light;
padding: 20px;
span.help-block {
.help-block {
padding-top: 7px;
margin-top: 0;
}
......@@ -226,13 +235,12 @@
}
#sidebar {
flex: 1;
height: 100%;
&.sidebar-mini {
display: inline-block;
vertical-align: top;
width: 20%;
border-right: 1px solid $white-normal;
height: calc(100vh + 20px);
overflow: auto;
}
......@@ -261,7 +269,6 @@
text-transform: uppercase;
font-weight: bold;
color: $gray-darkest;
width: 185px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
......@@ -270,7 +277,7 @@
}
}
.fa {
.file-icon {
margin-right: 5px;
}
......@@ -280,118 +287,22 @@
}
a {
@include str-truncated(250px);
color: $almost-black;
display: inline-block;
vertical-align: middle;
}
ul {
list-style-type: none;
padding: 0;
li {
border-bottom: 1px solid $border-gray-normal;
padding: 10px 20px;
a {
color: $almost-black;
}
.fa {
font-size: $code_font_size;
margin-right: 5px;
}
}
}
}
}
.animation-container {
background: $repo-editor-grey;
height: 40px;
overflow: hidden;
position: relative;
&.animation-container-small {
height: 12px;
}
&::before {
animation-duration: 1s;
animation-fill-mode: forwards;
animation-iteration-count: infinite;
animation-name: blockTextShine;
animation-timing-function: linear;
background-image: $repo-editor-linear-gradient;
background-repeat: no-repeat;
background-size: 800px 45px;
content: ' ';
display: block;
height: 100%;
position: relative;
}
div {
background: $white-light;
height: 6px;
left: 0;
position: absolute;
right: 0;
}
.line-of-code-1 {
left: 0;
top: 8px;
}
.line-of-code-2 {
left: 150px;
top: 0;
height: 10px;
}
.line-of-code-3 {
left: 0;
top: 23px;
}
.line-of-code-4 {
left: 0;
top: 38px;
}
.line-of-code-5 {
left: 200px;
top: 28px;
height: 10px;
}
.line-of-code-6 {
top: 14px;
left: 230px;
height: 10px;
}
}
.render-error {
min-height: calc(100vh - 63px);
min-height: calc(100vh - 62px);
p {
width: 100%;
}
}
@keyframes blockTextShine {
0% {
transform: translateX(-468px);
}
100% {
transform: translateX(468px);
}
}
@keyframes swipeRightAppear {
0% {
transform: scaleX(0.00);
......
......@@ -10,7 +10,7 @@ module IssuableActions
def destroy
issuable.destroy
destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym
TodoService.new.public_send(destroy_method, issuable, current_user)
TodoService.new.public_send(destroy_method, issuable, current_user) # rubocop:disable GitlabSecurity/PublicSend
name = issuable.human_class_name
flash[:notice] = "The #{name} was successfully deleted."
......
......@@ -64,7 +64,7 @@ class Import::GithubController < Import::BaseController
end
def import_enabled?
__send__("#{provider}_import_enabled?")
__send__("#{provider}_import_enabled?") # rubocop:disable GitlabSecurity/PublicSend
end
def new_import_url
......
......@@ -198,6 +198,10 @@ class Projects::BlobController < Projects::ApplicationController
json = blob_json(@blob)
return render_404 unless json
path_segments = @path.split('/')
path_segments.pop
tree_path = path_segments.join('/')
render json: json.merge(
path: blob.path,
name: blob.name,
......@@ -212,6 +216,7 @@ class Projects::BlobController < Projects::ApplicationController
raw_path: project_raw_path(project, @id),
blame_path: project_blame_path(project, @id),
commits_path: project_commits_path(project, @id),
tree_path: project_tree_path(project, File.join(@ref, tree_path)),
permalink: project_blob_path(project, File.join(@commit.id, @path))
)
end
......
......@@ -89,7 +89,7 @@ class UploadsController < ApplicationController
@uploader.retrieve_from_store!(params[:filename])
else
@uploader = @model.send(upload_mount)
@uploader = @model.public_send(upload_mount) # rubocop:disable GitlabSecurity/PublicSend
redirect_to @uploader.url unless @uploader.file_storage?
end
......
......@@ -128,10 +128,10 @@ module CommitsHelper
# avatar: true will prepend the avatar image
# size: size of the avatar image in px
def commit_person_link(commit, options = {})
user = commit.send(options[:source])
user = commit.public_send(options[:source]) # rubocop:disable GitlabSecurity/PublicSend
source_name = clean(commit.send "#{options[:source]}_name".to_sym)
source_email = clean(commit.send "#{options[:source]}_email".to_sym)
source_name = clean(commit.public_send(:"#{options[:source]}_name")) # rubocop:disable GitlabSecurity/PublicSend
source_email = clean(commit.public_send(:"#{options[:source]}_email")) # rubocop:disable GitlabSecurity/PublicSend
person_name = user.try(:name) || source_name
......
......@@ -5,7 +5,7 @@ module ImportHelper
end
def provider_project_link(provider, path_with_namespace)
url = __send__("#{provider}_project_url", path_with_namespace)
url = __send__("#{provider}_project_url", path_with_namespace) # rubocop:disable GitlabSecurity/PublicSend
link_to path_with_namespace, url, target: '_blank', rel: 'noopener noreferrer'
end
......
......@@ -181,7 +181,14 @@ module IssuablesHelper
end
def assigned_issuables_count(issuable_type)
current_user.public_send("assigned_open_#{issuable_type}_count")
case issuable_type
when :issues
current_user.assigned_open_issues_count
when :merge_requests
current_user.assigned_open_merge_requests_count
else
raise ArgumentError, "invalid issuable `#{issuable_type}`"
end
end
def issuable_filter_params
......@@ -306,10 +313,6 @@ module IssuablesHelper
cookies[:collapsed_gutter] == 'true'
end
def base_issuable_scope(issuable)
issuable.project.send(issuable.class.table_name).send(issuable_state_scope(issuable))
end
def issuable_state_scope(issuable)
if issuable.respond_to?(:merged?) && issuable.merged?
:merged
......
......@@ -32,7 +32,18 @@ module MilestonesHelper
end
def milestone_issues_by_label_count(milestone, label, state:)
milestone.issues.with_label(label.title).send(state).size
issues = milestone.issues.with_label(label.title)
issues =
case state
when :opened
issues.opened
when :closed
issues.closed
else
raise ArgumentError, "invalid milestone state `#{state}`"
end
issues.size
end
# Returns count of milestones for different states
......
......@@ -149,15 +149,16 @@ module ProjectsHelper
# Don't show option "everyone with access" if project is private
options = project_feature_options
level = @project.project_feature.public_send(field) # rubocop:disable GitlabSecurity/PublicSend
if @project.private?
level = @project.project_feature.send(field)
disabled_option = ProjectFeature::ENABLED
highest_available_option = ProjectFeature::PRIVATE if level == disabled_option
end
options = options_for_select(
options.invert,
selected: highest_available_option || @project.project_feature.public_send(field),
selected: highest_available_option || level,
disabled: disabled_option
)
......@@ -519,7 +520,7 @@ module ProjectsHelper
end
def filename_path(project, filename)
if project && blob = project.repository.send(filename)
if project && blob = project.repository.public_send(filename) # rubocop:disable GitlabSecurity/PublicSend
project_blob_path(
project,
tree_join(project.default_branch, blob.name)
......
......@@ -2,7 +2,7 @@ module VersionCheckHelper
def version_status_badge
if Rails.env.production? && current_application_settings.version_check_enabled
image_url = VersionCheck.new.url
image_tag image_url, class: 'js-version-status-badge', lazy: false
image_tag image_url, class: 'js-version-status-badge'
end
end
end
......@@ -200,7 +200,7 @@ class Commit
end
def method_missing(m, *args, &block)
@raw.send(m, *args, &block)
@raw.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
def respond_to_missing?(method, include_private = false)
......
......@@ -78,7 +78,7 @@ module CacheMarkdownField
def cached_html_up_to_date?(markdown_field)
html_field = cached_markdown_fields.html_field(markdown_field)
cached = cached_html_for(markdown_field).present? && __send__(markdown_field).present?
cached = cached_html_for(markdown_field).present? && __send__(markdown_field).present? # rubocop:disable GitlabSecurity/PublicSend
return false unless cached
markdown_changed = attribute_changed?(markdown_field) || false
......@@ -93,14 +93,14 @@ module CacheMarkdownField
end
def attribute_invalidated?(attr)
__send__("#{attr}_invalidated?")
__send__("#{attr}_invalidated?") # rubocop:disable GitlabSecurity/PublicSend
end
def cached_html_for(markdown_field)
raise ArgumentError.new("Unknown field: #{field}") unless
cached_markdown_fields.markdown_fields.include?(markdown_field)
__send__(cached_markdown_fields.html_field(markdown_field))
__send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend
end
included do
......
......@@ -65,7 +65,7 @@ module Elastic
end
TRACKED_FEATURE_SETTINGS.each do |feature|
data[feature] = project_feature.public_send(feature)
data[feature] = project_feature.public_send(feature) # rubocop:disable GitlabSecurity/PublicSend
end
data
......
......@@ -9,7 +9,7 @@ module InternalId
def set_iid
if iid.blank?
parent = project || group
records = parent.send(self.class.name.tableize)
records = parent.public_send(self.class.name.tableize) # rubocop:disable GitlabSecurity/PublicSend
records = records.with_deleted if self.paranoid?
max_iid = records.maximum(:iid)
......
......@@ -56,7 +56,7 @@ module Mentionable
end
self.class.mentionable_attrs.each do |attr, options|
text = __send__(attr)
text = __send__(attr) # rubocop:disable GitlabSecurity/PublicSend
options = options.merge(
cache_key: [self, attr],
author: author,
......@@ -100,7 +100,7 @@ module Mentionable
end
self.class.mentionable_attrs.any? do |attr, _|
__send__(attr) =~ reference_pattern
__send__(attr) =~ reference_pattern # rubocop:disable GitlabSecurity/PublicSend
end
end
......
......@@ -82,7 +82,7 @@ module Participable
if attr.respond_to?(:call)
source.instance_exec(current_user, ext, &attr)
else
process << source.__send__(attr)
process << source.__send__(attr) # rubocop:disable GitlabSecurity/PublicSend
end
end
when Enumerable, ActiveRecord::Relation
......
......@@ -32,6 +32,6 @@ module ProjectFeaturesCompatibility
build_project_feature unless project_feature
access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED
project_feature.send(:write_attribute, field, access_level)
project_feature.__send__(:write_attribute, field, access_level) # rubocop:disable GitlabSecurity/PublicSend
end
end
......@@ -246,7 +246,7 @@ class License < ActiveRecord::Base
if License.column_names.include?(method_name.to_s)
super
elsif license && license.respond_to?(method_name)
license.send(method_name, *arguments, &block)
license.__send__(method_name, *arguments, &block) # rubocop:disable GitlabSecurity/PublicSend
else
super
end
......
......@@ -12,7 +12,7 @@ module Network
end
def method_missing(m, *args, &block)
@commit.send(m, *args, &block)
@commit.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
def space
......
......@@ -368,7 +368,10 @@ class Project < ActiveRecord::Base
state :failed
after_transition [:none, :finished, :failed] => :scheduled do |project, _|
project.run_after_commit { add_import_job }
project.run_after_commit do
job_id = add_import_job
update(import_jid: job_id) if job_id
end
end
after_transition started: :finished do |project, _|
......@@ -523,17 +526,26 @@ class Project < ActiveRecord::Base
def add_import_job
job_id =
if forked?
RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path,
RepositoryForkWorker.perform_async(id,
forked_from_project.repository_storage_path,
forked_from_project.full_path,
self.namespace.full_path)
else
RepositoryImportWorker.perform_async(self.id)
end
log_import_activity(job_id)
job_id
end
def log_import_activity(job_id, type: :import)
job_type = type.to_s.capitalize
if job_id
Rails.logger.info "Import job started for #{full_path} with job ID #{job_id}"
Rails.logger.info("#{job_type} job scheduled for #{full_path} with job ID #{job_id}.")
else
Rails.logger.error "Import job failed to start for #{full_path}"
Rails.logger.error("#{job_type} job failed to create for #{full_path}.")
end
end
......@@ -542,6 +554,7 @@ class Project < ActiveRecord::Base
ProjectCacheWorker.perform_async(self.id)
end
update(import_error: nil)
remove_import_data
end
......@@ -919,14 +932,14 @@ class Project < ActiveRecord::Base
end
def execute_hooks(data, hooks_scope = :push_hooks)
hooks.send(hooks_scope).each do |hook|
hooks.public_send(hooks_scope).each do |hook| # rubocop:disable GitlabSecurity/PublicSend
hook.async_execute(data, hooks_scope.to_s)
end
end
def execute_services(data, hooks_scope = :push_hooks)
# Call only service hooks that are active for this scope
services.send(hooks_scope).each do |service|
services.public_send(hooks_scope).each do |service| # rubocop:disable GitlabSecurity/PublicSend
service.async_execute(data)
end
end
......
......@@ -115,7 +115,7 @@ class ChatNotificationService < Service
def get_channel_field(event)
field_name = event_channel_name(event)
self.public_send(field_name)
self.public_send(field_name) # rubocop:disable GitlabSecurity/PublicSend
end
def build_event_channels
......
......@@ -53,7 +53,7 @@ class HipchatService < Service
return unless supported_events.include?(data[:object_kind])
message = create_message(data)
return unless message.present?
gate[room].send('GitLab', message, message_options(data))
gate[room].send('GitLab', message, message_options(data)) # rubocop:disable GitlabSecurity/PublicSend
end
def test(data)
......
class ProtectableDropdown
REF_TYPES = %i[branches tags].freeze
def initialize(project, ref_type)
raise ArgumentError, "invalid ref type `#{ref_type}`" unless ref_type.in?(REF_TYPES)
@project = project
@ref_type = ref_type
end
......@@ -16,7 +20,7 @@ class ProtectableDropdown
private
def refs
@project.repository.public_send(@ref_type)
@project.repository.public_send(@ref_type) # rubocop:disable GitlabSecurity/PublicSend
end
def ref_names
......@@ -24,7 +28,7 @@ class ProtectableDropdown
end
def protections
@project.public_send("protected_#{@ref_type}")
@project.public_send("protected_#{@ref_type}") # rubocop:disable GitlabSecurity/PublicSend
end
def non_wildcard_protected_ref_names
......
......@@ -55,7 +55,9 @@ class Repository
alias_method(original, name)
define_method(name) do
cache_method_output(name, fallback: fallback, memoize_only: memoize_only) { __send__(original) }
cache_method_output(name, fallback: fallback, memoize_only: memoize_only) do
__send__(original) # rubocop:disable GitlabSecurity/PublicSend
end
end
end
......@@ -450,9 +452,9 @@ class Repository
def method_missing(m, *args, &block)
if m == :lookup && !block_given?
lookup_cache[m] ||= {}
lookup_cache[m][args.join(":")] ||= raw_repository.send(m, *args, &block)
lookup_cache[m][args.join(":")] ||= raw_repository.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
else
raw_repository.send(m, *args, &block)
raw_repository.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
end
......@@ -783,7 +785,7 @@ class Repository
end
actions.each do |options|
index.public_send(options.delete(:action), options)
index.public_send(options.delete(:action), options) # rubocop:disable GitlabSecurity/PublicSend
end
options = {
......@@ -987,7 +989,7 @@ class Repository
upstream_commit = commit("refs/remotes/#{MIRROR_REMOTE}/#{branch_name}")
if upstream_commit
!is_ancestor?(branch_commit.id, upstream_commit.id)
!rugged_is_ancestor?(branch_commit.id, upstream_commit.id)
else
false
end
......@@ -998,7 +1000,7 @@ class Repository
upstream_commit = commit("refs/remotes/#{remote_ref}/#{branch_name}")
if upstream_commit
!is_ancestor?(upstream_commit.id, branch_commit.id)
!rugged_is_ancestor?(upstream_commit.id, branch_commit.id)
else
false
end
......
......@@ -1093,7 +1093,7 @@ class User < ActiveRecord::Base
# Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
def send_devise_notification(notification, *args)
return true unless can?(:receive_notifications)
devise_mailer.send(notification, self, *args).deliver_later
devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend
end
# This works around a bug in Devise 4.2.0 that erroneously causes a user to
......
# TODO: Inherit from TreeEntity, when `Tree` implements `id` and `name` like `Gitlab::Git::Tree`.
class TreeRootEntity < Grape::Entity
include RequestAwareEntity
expose :path
expose :trees, using: TreeEntity
expose :blobs, using: BlobEntity
expose :submodules, using: SubmoduleEntity
expose :parent_tree_url do |tree|
path = tree.path.sub(%r{\A/}, '')
next unless path.present?
path_segments = path.split('/')
path_segments.pop
parent_tree_path = path_segments.join('/')
project_tree_path(request.project, File.join(request.ref, parent_tree_path))
end
end
......@@ -58,7 +58,7 @@ class AkismetService
}
begin
akismet_client.public_send(type, options[:ip_address], options[:user_agent], params)
akismet_client.public_send(type, options[:ip_address], options[:user_agent], params) # rubocop:disable GitlabSecurity/PublicSend
true
rescue => e
Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
......
......@@ -23,7 +23,7 @@ module Ci
end
attributes = CLONE_ACCESSORS.map do |attribute|
[attribute, build.send(attribute)]
[attribute, build.public_send(attribute)] # rubocop:disable GitlabSecurity/PublicSend
end
attributes.push([:user, current_user])
......
......@@ -11,6 +11,7 @@ module Commits
def commit_change(action)
raise NotImplementedError unless repository.respond_to?(action)
# rubocop:disable GitlabSecurity/PublicSend
repository.public_send(
action,
current_user,
......
......@@ -19,7 +19,7 @@ module Geo
::Gitlab::Geo.secondary_nodes.each do |node|
next unless node.enabled?
notify_url = node.send(notify_url_method.to_sym)
notify_url = node.__send__(notify_url_method.to_sym) # rubocop:disable GitlabSecurity/PublicSend
success, details = notify(notify_url, content)
unless success
......
......@@ -340,7 +340,7 @@ class IssuableBaseService < BaseService
def invalidate_cache_counts(issuable, users: [], skip_project_cache: false)
users.each do |user|
user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts")
user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") # rubocop:disable GitlabSecurity/PublicSend
end
unless skip_project_cache
......
......@@ -21,8 +21,9 @@ module IssueLinks
end
end
# Returns a Boolean indicating if the Issue was related.
def relate_issues(referenced_issue)
IssueLink.create(source: @issue, target: referenced_issue)
IssueLink.new(source: @issue, target: referenced_issue).save
end
def create_notes(referenced_issue)
......
......@@ -36,7 +36,7 @@ module Members
source.members.find_by(condition) ||
source.requesters.find_by!(condition)
else
source.public_send(scope).find_by!(condition)
source.public_send(scope).find_by!(condition) # rubocop:disable GitlabSecurity/PublicSend
end
end
......
# rubocop:disable GitlabSecurity/PublicSend
# NotificationService class
#
# Used for notifying users with emails about different events
......
......@@ -6,7 +6,7 @@ class SystemHooksService
end
def execute_hooks(data, hooks_scope = :all)
SystemHook.public_send(hooks_scope).find_each do |hook|
SystemHook.public_send(hooks_scope).find_each do |hook| # rubocop:disable GitlabSecurity/PublicSend
hook.async_execute(data, 'system_hooks')
end
end
......
......@@ -18,7 +18,7 @@ module TestHooks
end
error_message = catch(:validation_error) do
sample_data = self.__send__(trigger_data_method)
sample_data = self.__send__(trigger_data_method) # rubocop:disable GitlabSecurity/PublicSend
return hook.execute(sample_data, trigger)
end
......
......@@ -38,7 +38,7 @@ class ObjectStoreUploader < CarrierWave::Uploader::Base
end
def real_object_store
subject.public_send(:"#{field}_store")
subject.public_send(:"#{field}_store") # rubocop:disable GitlabSecurity/PublicSend
end
def object_store
......@@ -47,7 +47,7 @@ class ObjectStoreUploader < CarrierWave::Uploader::Base
def object_store=(value)
@storage = nil
subject.public_send(:"#{field}_store=", value)
subject.public_send(:"#{field}_store=", value) # rubocop:disable GitlabSecurity/PublicSend
end
def use_file
......
......@@ -111,7 +111,7 @@
= link_to admin_license_path, title: 'License' do
.nav-icon-container
= custom_icon('license')
%span
%span.nav-item-name
License
- if akismet_enabled?
......@@ -126,14 +126,14 @@
= link_to admin_push_rule_path, title: 'Push Rules' do
.nav-icon-container
= custom_icon('push_rules')
%span
%span.nav-item-name
Push Rules
= nav_link(controller: :geo_nodes) do
= link_to admin_geo_nodes_path, title: 'Geo Nodes' do
.nav-icon-container
= custom_icon('geo_nodes')
%span
%span.nav-item-name
Geo Nodes
= nav_link(controller: :deploy_keys) do
......
......@@ -89,7 +89,7 @@
= link_to profile_pipeline_quota_path, title: 'Pipeline quota' do
.nav-icon-container
= custom_icon('pipeline')
%span
%span.nav-item-name
Pipeline quota
= render 'shared/sidebar_toggle_button'
......@@ -4,7 +4,7 @@
= link_to 'Close merge request', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-close close-mr-link js-note-target-close", title: "Close merge request", data: { original_text: "Close merge request", alternative_text: "Comment & close merge request"}
- if @merge_request.reopenable?
= link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-reopen reopen-mr-link js-note-target-close js-note-target-reopen", title: "Reopen merge request", data: { original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"}
%comment-and-resolve-btn{ "inline-template" => true, ":discussion-id" => "" }
%comment-and-resolve-btn{ "inline-template" => true }
%button.btn.btn-nr.btn-default.append-right-10.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { project_path: "#{project_path(@merge_request.project)}" } }
{{ buttonText }}
......
......@@ -6,7 +6,7 @@
- @options && @options.each do |key, value|
= hidden_field_tag key, value, id: nil
.dropdown
= dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project), field_name: 'ref', submit_form_on_click: true }, { toggle_class: "js-project-refs-dropdown" }
= dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown" }
.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
= dropdown_title _("Switch branch/tag")
= dropdown_filter _("Search branches and tags")
......
- dropdown_toggle_text = @ref || @project.default_branch
= form_tag nil, method: :get, class: "project-refs-target-form" do
= form_tag nil, method: :get, style: { display: 'none' }, class: "project-refs-target-form" do
= hidden_field_tag :destination, destination
- if defined?(path)
= hidden_field_tag :path, path
......
#repo{ data: { url: content_url, project_name: project.name, refs_url: refs_project_path(project, format: :json), project_url: project_path(project), project_id: project.id, can_commit: (!!can_push_branch?(project, @ref)).to_s } }
%repo
#repo{ data: { url: content_url,
project_name: project.name,
refs_url: refs_project_path(project, format: :json),
project_url: project_path(project),
project_id: project.id,
can_commit: (!!can_push_branch?(project, @ref)).to_s,
on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s } }
......@@ -19,9 +19,9 @@ class ElasticIndexerWorker
record.__elasticsearch__.client = client
if klass.nested?
record.__elasticsearch__.__send__ "#{operation}_document", parent: record.es_parent
record.__elasticsearch__.__send__ "#{operation}_document", parent: record.es_parent # rubocop:disable GitlabSecurity/PublicSend
else
record.__elasticsearch__.__send__ "#{operation}_document"
record.__elasticsearch__.__send__ "#{operation}_document" # rubocop:disable GitlabSecurity/PublicSend
end
update_issue_notes(record, options["changed_fields"]) if klass == Issue
......
......@@ -4,6 +4,6 @@ class GitlabShellWorker
include DedicatedSidekiqQueue
def perform(action, *arg)
gitlab_shell.send(action, *arg)
gitlab_shell.__send__(action, *arg) # rubocop:disable GitlabSecurity/PublicSend
end
end
......@@ -5,14 +5,17 @@ class RepositoryForkWorker
include Gitlab::ShellAdapter
include DedicatedSidekiqQueue
sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
def perform(project_id, forked_from_repository_storage_path, source_path, target_path)
project = Project.find(project_id)
return unless start_fork(project)
Gitlab::Metrics.add_event(:fork_repository,
source_path: source_path,
target_path: target_path)
project = Project.find(project_id)
project.import_start
result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_path,
project.repository_storage_path, target_path)
raise ForkError, "Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}" unless result
......@@ -33,6 +36,13 @@ class RepositoryForkWorker
private
def start_fork(project)
return true if project.import_start
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while forking.")
false
end
def fail_fork(project, message)
Rails.logger.error(message)
project.mark_import_as_failed(message)
......
......@@ -4,23 +4,18 @@ class RepositoryImportWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_EXPIRATION
attr_accessor :project, :current_user
sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
def perform(project_id)
@project = Project.find(project_id)
@current_user = @project.creator
project = Project.find(project_id)
project.import_start
return unless start_import(project)
Gitlab::Metrics.add_event(:import_repository,
import_url: @project.import_url,
path: @project.full_path)
project.update_columns(import_jid: self.jid, import_error: nil)
import_url: project.import_url,
path: project.full_path)
result = Projects::ImportService.new(project, current_user).execute
result = Projects::ImportService.new(project, project.creator).execute
raise ImportError, result[:message] if result[:status] == :error
project.repository.after_import
......@@ -41,6 +36,13 @@ class RepositoryImportWorker
private
def start_import(project)
return true if project.import_start
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.")
false
end
def fail_import(project, message)
project.mark_import_as_failed(message)
end
......
class RepositoryUpdateMirrorWorker
UpdateError = Class.new(StandardError)
UpdateAlreadyInProgressError = Class.new(StandardError)
include Sidekiq::Worker
include Gitlab::ShellAdapter
include DedicatedSidekiqQueue
LEASE_KEY = 'repository_update_mirror_worker_start_scheduler'.freeze
LEASE_TIMEOUT = 2.seconds
# Retry not neccessary. It will try again at the next update interval.
sidekiq_options retry: false
sidekiq_options retry: false, status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
attr_accessor :project, :repository, :current_user
def perform(project_id)
project = Project.find(project_id)
raise UpdateAlreadyInProgressError if project.import_started?
start_mirror(project)
return unless start_mirror(project)
@current_user = project.mirror_user || project.creator
......@@ -23,8 +24,6 @@ class RepositoryUpdateMirrorWorker
raise UpdateError, result[:message] if result[:status] == :error
finish_mirror(project)
rescue UpdateAlreadyInProgressError
raise
rescue UpdateError => ex
fail_mirror(project, ex.message)
raise
......@@ -34,16 +33,27 @@ class RepositoryUpdateMirrorWorker
fail_mirror(project, ex.message)
raise UpdateError, "#{ex.class}: #{ex.message}"
ensure
UpdateAllMirrorsWorker.perform_async if Gitlab::Mirror.threshold_reached?
if !lease.exists? && Gitlab::Mirror.reschedule_immediately? && lease.try_obtain
UpdateAllMirrorsWorker.perform_async
end
end
private
def start_mirror(project)
project.import_start
def lease
@lease ||= ::Gitlab::ExclusiveLease.new(LEASE_KEY, timeout: LEASE_TIMEOUT)
end
def start_mirror(project)
if project.import_start
Gitlab::Mirror.increment_metric(:mirrors_running, 'Mirrors running count')
Rails.logger.info("Mirror update for #{project.full_path} started. Waiting duration: #{project.mirror_waiting_duration}")
true
else
Rails.logger.info("Project #{project.full_path} was in inconsistent state: #{project.import_status}")
false
end
end
def fail_mirror(project, message)
......
......@@ -2,36 +2,60 @@ class StuckImportJobsWorker
include Sidekiq::Worker
include CronjobQueue
IMPORT_EXPIRATION = 15.hours.to_i
IMPORT_JOBS_EXPIRATION = 15.hours.to_i
def perform
stuck_projects.find_in_batches(batch_size: 500) do |group|
projects_without_jid_count = mark_projects_without_jid_as_failed!
projects_with_jid_count = mark_projects_with_jid_as_failed!
Gitlab::Metrics.add_event(:stuck_import_jobs,
projects_without_jid_count: projects_without_jid_count,
projects_with_jid_count: projects_with_jid_count)
end
private
def mark_projects_without_jid_as_failed!
started_projects_without_jid.each do |project|
project.mark_import_as_failed(error_message)
end.count
end
def mark_projects_with_jid_as_failed!
completed_jids_count = 0
started_projects_with_jid.find_in_batches(batch_size: 500) do |group|
jids = group.map(&:import_jid)
# Find the jobs that aren't currently running or that exceeded the threshold.
completed_jids = Gitlab::SidekiqStatus.completed_jids(jids)
completed_jids = Gitlab::SidekiqStatus.completed_jids(jids).to_set
if completed_jids.any?
completed_ids = group.select { |project| completed_jids.include?(project.import_jid) }.map(&:id)
fail_batch!(completed_jids, completed_ids)
completed_jids_count += completed_jids.count
group.each do |project|
project.mark_import_as_failed(error_message) if completed_jids.include?(project.import_jid)
end
Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_jids.to_a.join(', ')}")
end
end
private
completed_jids_count
end
def stuck_projects
Project.select('id, import_jid').with_import_status(:started).where.not(import_jid: nil)
def started_projects
Project.with_import_status(:started)
end
def fail_batch!(completed_jids, completed_ids)
Project.where(id: completed_ids).update_all(import_status: 'failed', import_error: error_message)
def started_projects_with_jid
started_projects.where.not(import_jid: nil)
end
Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_jids.join(', ')}")
def started_projects_without_jid
started_projects.where(import_jid: nil)
end
def error_message
"Import timed out. Import took longer than #{IMPORT_EXPIRATION} seconds"
"Import timed out. Import took longer than #{IMPORT_JOBS_EXPIRATION} seconds"
end
end
......@@ -9,19 +9,11 @@ class UpdateAllMirrorsWorker
lease_uuid = try_obtain_lease
return unless lease_uuid
fail_stuck_mirrors!
schedule_mirrors!
cancel_lease(lease_uuid)
end
def fail_stuck_mirrors!
Project.stuck_mirrors.find_each(batch_size: 50) do |project|
project.mark_import_as_failed('The mirror update took too long to complete.')
end
end
def schedule_mirrors!
capacity = batch_size = Gitlab::Mirror.available_capacity
......@@ -59,12 +51,7 @@ class UpdateAllMirrorsWorker
end
def pull_mirrors_batch(freeze_at:, batch_size:, offset_at: nil)
relation = Project
.mirror
.joins(:mirror_data)
.where("next_execution_timestamp <= ? AND import_status NOT IN ('scheduled', 'started')", freeze_at)
.reorder('project_mirror_data.next_execution_timestamp')
.limit(batch_size)
relation = Project.mirrors_to_sync(freeze_at).reorder('project_mirror_data.next_execution_timestamp').limit(batch_size)
relation = relation.where('next_execution_timestamp > ?', offset_at) if offset_at
......
---
title: Improves handling of stuck imports.
merge_request: 2628
author:
---
title: Improves handling of the mirror threshold.
merge_request: 2671
author:
---
title: Fix Copy to Clipboard for SSH Public Key on Pull Repository settings
merge_request: 2692
author:
type: fixed
---
title: Create system notes only if issue was successfully related
merge_request:
author:
type: fixed
---
title: Support handling of rename events in Geo Log Cursor
merge_request:
author:
# rubocop:disable GitlabSecurity/PublicSend
require_dependency Rails.root.join('lib/gitlab') # Load Gitlab as soon as possible
class Settings < Settingslogic
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment