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