diff options
Diffstat (limited to 'app')
108 files changed, 1362 insertions, 1134 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 76b724e1bcb..56f91e95bb9 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -97,7 +97,6 @@ const Api = { }, commitMultiple(id, data, callback) { - // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions const url = Api.buildUrl(Api.commitPath) .replace(':id', id); return $.ajax({ diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 2b0bf49cf92..047544b1762 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -17,7 +17,7 @@ window.CommitsList = (function() { } }); - Pager.init(limit, false, false, this.processCommits); + Pager.init(parseInt(limit, 10), false, false, this.processCommits); this.content = $("#commits-list"); this.searchField = $("#commits-search"); diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js index a2d33b0936e..5decfc1dc01 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js @@ -42,6 +42,10 @@ $(() => { $components.each(function () { const $this = $(this); const noteId = $this.attr(':note-id'); + const discussionId = $this.attr(':discussion-id'); + + if ($this.is('comment-and-resolve-btn') && !discussionId) return; + const tmp = Vue.extend({ template: $this.get(0).outerHTML }); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index de47485c9f2..a0ed5c23ffe 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -644,7 +644,7 @@ import initChangesDropdown from './init_changes_dropdown'; return Dispatcher; })(); - $(function() { + $(window).on('load', function() { new Dispatcher(); }); }).call(window); diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index cbc3ad23990..32cb42c8b10 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -15,6 +15,10 @@ export const setOpenMenu = (menu = null) => { currentOpenMenu = menu; }; export const slope = (a, b) => (b.y - a.y) / (b.x - a.x); +let headerHeight = 50; + +export const getHeaderHeight = () => headerHeight; + export const canShowActiveSubItems = (el) => { const isHiddenByMedia = bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md'; @@ -74,7 +78,7 @@ export const moveSubItemsToPosition = (el, subItems) => { const isAbove = top < boundingRect.top; subItems.classList.add('fly-out-list'); - subItems.style.transform = `translate3d(0, ${Math.floor(top)}px, 0)`; // eslint-disable-line no-param-reassign + subItems.style.transform = `translate3d(0, ${Math.floor(top) - headerHeight}px, 0)`; // eslint-disable-line no-param-reassign const subItemsRect = subItems.getBoundingClientRect(); @@ -153,6 +157,8 @@ export default () => { }, getHideSubItemsInterval()); }); + headerHeight = document.querySelector('.nav-sidebar').offsetTop; + items.forEach((el) => { const subItems = el.querySelector('.sidebar-sub-level-items'); diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index b5975295329..4d629bc6326 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -111,8 +111,7 @@ window.GroupsSelect = (function() { }; GroupsSelect.prototype.forceOverflow = function (e) { - const itemHeight = this.dropdown.querySelector('.select2-result:first-child').clientHeight; - this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight - (itemHeight * 0.9))}px`; + this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight)}px`; }; GroupsSelect.PER_PAGE = 20; diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js index 43a808b6ab3..ff2b66046b4 100644 --- a/app/assets/javascripts/lib/utils/sticky.js +++ b/app/assets/javascripts/lib/utils/sticky.js @@ -1,7 +1,7 @@ export const isSticky = (el, scrollY, stickyTop) => { const top = el.offsetTop - scrollY; - if (top === stickyTop) { + if (top <= stickyTop) { el.classList.add('is-stuck'); } else { el.classList.remove('is-stuck'); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 37f531c78f4..6d7c7e3c930 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -132,8 +132,9 @@ import './project_select'; import './project_show'; import './project_variables'; import './projects_list'; -import './render_gfm'; +import './syntax_highlight'; import './render_math'; +import './render_gfm'; import './right_sidebar'; import './search'; import './search_autocomplete'; @@ -141,7 +142,6 @@ import './smart_interval'; import './star'; import './subscription'; import './subscription_select'; -import './syntax_highlight'; import './dispatcher'; diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/new_sidebar.js index b10b074f5ac..2d1ed9e4076 100644 --- a/app/assets/javascripts/new_sidebar.js +++ b/app/assets/javascripts/new_sidebar.js @@ -43,10 +43,12 @@ export default class NewNavSidebar { } toggleCollapsedSidebar(collapsed) { - this.$sidebar.toggleClass('sidebar-icons-only', collapsed); + const breakpoint = bp.getBreakpointSize(); + if (this.$sidebar.length) { + this.$sidebar.toggleClass('sidebar-icons-only', collapsed); this.$page.toggleClass('page-with-new-sidebar', !collapsed); - this.$page.toggleClass('page-with-icon-sidebar', collapsed); + this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed); } NewNavSidebar.setCollapsedCookie(collapsed); } diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 1c2100a1c25..d7e3ab42f00 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -126,11 +126,11 @@ import Cookies from 'js-cookie'; var $form = $dropdown.closest('form'); var $visit = $dropdown.data('visit'); - var shouldVisit = typeof $visit === 'undefined' ? true : $visit; + var shouldVisit = $visit ? true : $visit; var action = $form.attr('action'); var divider = action.indexOf('?') === -1 ? '?' : '&'; if (shouldVisit) { - gl.utils.visitUrl(action + '' + divider + '' + $form.serialize()); + gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`); } } } diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js index 2c3a9cacd38..bcdc0fd67b8 100644 --- a/app/assets/javascripts/render_gfm.js +++ b/app/assets/javascripts/render_gfm.js @@ -11,7 +11,5 @@ return this; }; - $(document).on('ready load', function() { - return $('body').renderGFM(); - }); + $(() => $('body').renderGFM()); }).call(window); diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue index 703da749ad3..d6c864cb976 100644 --- a/app/assets/javascripts/repo/components/repo.vue +++ b/app/assets/javascripts/repo/components/repo.vue @@ -14,13 +14,13 @@ export default { data: () => Store, mixins: [RepoMixin], components: { - 'repo-sidebar': RepoSidebar, - 'repo-tabs': RepoTabs, - 'repo-file-buttons': RepoFileButtons, + RepoSidebar, + RepoTabs, + RepoFileButtons, 'repo-editor': MonacoLoaderHelper.repoEditorLoader, - 'repo-commit-section': RepoCommitSection, - 'popup-dialog': PopupDialog, - 'repo-preview': RepoPreview, + RepoCommitSection, + PopupDialog, + RepoPreview, }, mounted() { @@ -28,12 +28,12 @@ export default { }, methods: { - dialogToggled(toggle) { + toggleDialogOpen(toggle) { this.dialog.open = toggle; }, dialogSubmitted(status) { - this.dialog.open = false; + this.toggleDialogOpen(false); this.dialog.status = status; }, @@ -43,21 +43,28 @@ export default { </script> <template> -<div class="repository-view tree-content-holder"> - <repo-sidebar/><div class="panel-right" :class="{'edit-mode': editMode}"> - <repo-tabs/> - <component :is="currentBlobView" class="blob-viewer-container"></component> - <repo-file-buttons/> + <div class="repository-view"> + <div class="tree-content-holder" :class="{'tree-content-holder-mini' : isMini}"> + <repo-sidebar/> + <div v-if="isMini" + class="panel-right" + :class="{'edit-mode': editMode}"> + <repo-tabs/> + <component + :is="currentBlobView" + class="blob-viewer-container"/> + <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> - <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> diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue index bd83f80c928..5ec4a9b6593 100644 --- a/app/assets/javascripts/repo/components/repo_commit_section.vue +++ b/app/assets/javascripts/repo/components/repo_commit_section.vue @@ -2,18 +2,20 @@ /* global Flash */ import Store from '../stores/repo_store'; import RepoMixin from '../mixins/repo_mixin'; -import Helper from '../helpers/repo_helper'; import Service from '../services/repo_service'; -const RepoCommitSection = { +export default { data: () => Store, mixins: [RepoMixin], computed: { + showCommitable() { + return this.isCommitable && this.changedFiles.length; + }, + branchPaths() { - const branch = Helper.getBranch(); - return this.changedFiles.map(f => Helper.getFilePathFromFullPath(f.url, branch)); + return this.changedFiles.map(f => f.path); }, cantCommitYet() { @@ -28,11 +30,10 @@ const RepoCommitSection = { methods: { makeCommit() { // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions - const branch = Helper.getBranch(); const commitMessage = this.commitMessage; const actions = this.changedFiles.map(f => ({ action: 'update', - file_path: Helper.getFilePathFromFullPath(f.url, branch), + file_path: f.path, content: f.newContent, })); const payload = { @@ -47,51 +48,80 @@ const RepoCommitSection = { resetCommitState() { this.submitCommitsLoading = false; this.changedFiles = []; - this.openedFiles = []; this.commitMessage = ''; this.editMode = false; - $('html, body').animate({ scrollTop: 0 }, 'fast'); + window.scrollTo(0, 0); }, }, }; - -export default RepoCommitSection; </script> <template> -<div id="commit-area" v-if="isCommitable && changedFiles.length" > - <form class="form-horizontal"> +<div + v-if="showCommitable" + id="commit-area"> + <form + class="form-horizontal" + @submit.prevent="makeCommit"> <fieldset> <div class="form-group"> - <label class="col-md-4 control-label staged-files">Staged files ({{changedFiles.length}})</label> - <div class="col-md-4"> + <label class="col-md-4 control-label staged-files"> + Staged files ({{changedFiles.length}}) + </label> + <div class="col-md-6"> <ul class="list-unstyled changed-files"> - <li v-for="file in branchPaths" :key="file.id"> - <span class="help-block">{{file}}</span> + <li + v-for="branchPath in branchPaths" + :key="branchPath"> + <span class="help-block"> + {{branchPath}} + </span> </li> </ul> </div> </div> - <!-- Textarea - --> <div class="form-group"> - <label class="col-md-4 control-label" for="commit-message">Commit message</label> - <div class="col-md-4"> - <textarea class="form-control" id="commit-message" name="commit-message" v-model="commitMessage"></textarea> + <label + class="col-md-4 control-label" + for="commit-message"> + Commit message + </label> + <div class="col-md-6"> + <textarea + id="commit-message" + class="form-control" + name="commit-message" + v-model="commitMessage"> + </textarea> </div> </div> - <!-- Button Drop Down - --> <div class="form-group target-branch"> - <label class="col-md-4 control-label" for="target-branch">Target branch</label> - <div class="col-md-4"> - <span class="help-block">{{targetBranch}}</span> + <label + class="col-md-4 control-label" + for="target-branch"> + Target branch + </label> + <div class="col-md-6"> + <span class="help-block"> + {{targetBranch}} + </span> </div> </div> - <div class="col-md-offset-4 col-md-4"> - <button type="submit" :disabled="cantCommitYet" class="btn btn-success submit-commit" @click.prevent="makeCommit"> - <i class="fa fa-spinner fa-spin" v-if="submitCommitsLoading"></i> - <span class="commit-summary">Commit {{changedFiles.length}} {{filePluralize}}</span> + <div class="col-md-offset-4 col-md-6"> + <button + ref="submitCommit" + type="submit" + :disabled="cantCommitYet" + class="btn btn-success"> + <i + v-if="submitCommitsLoading" + class="fa fa-spinner fa-spin" + aria-hidden="true" + aria-label="loading"> + </i> + <span class="commit-summary"> + Commit {{changedFiles.length}} {{filePluralize}} + </span> </button> </div> </fieldset> diff --git a/app/assets/javascripts/repo/components/repo_edit_button.vue b/app/assets/javascripts/repo/components/repo_edit_button.vue index f47b6c33fa2..29b76975561 100644 --- a/app/assets/javascripts/repo/components/repo_edit_button.vue +++ b/app/assets/javascripts/repo/components/repo_edit_button.vue @@ -10,12 +10,15 @@ export default { return this.editMode ? this.__('Cancel edit') : this.__('Edit'); }, - buttonIcon() { - return this.editMode ? [] : ['fa', 'fa-pencil']; + showButton() { + return this.isCommitable && + !this.activeFile.render_error && + !this.binary && + this.openedFiles.length; }, }, methods: { - editClicked() { + editCancelClicked() { if (this.changedFiles.length) { this.dialog.open = true; return; @@ -23,25 +26,33 @@ export default { this.editMode = !this.editMode; Store.toggleBlobView(); }, + toggleProjectRefsForm() { + $('.project-refs-form').toggleClass('disabled', this.editMode); + $('.js-tree-ref-target-holder').toggle(this.editMode); + }, }, watch: { editMode() { - if (this.editMode) { - $('.project-refs-form').addClass('disabled'); - $('.js-tree-ref-target-holder').show(); - } else { - $('.project-refs-form').removeClass('disabled'); - $('.js-tree-ref-target-holder').hide(); - } + this.toggleProjectRefsForm(); }, }, }; </script> <template> -<button class="btn btn-default" @click.prevent="editClicked" v-cloak v-if="isCommitable && !activeFile.render_error" :disabled="binary"> - <i :class="buttonIcon"></i> - <span>{{buttonLabel}}</span> +<button + v-if="showButton" + class="btn btn-default" + type="button" + @click.prevent="editCancelClicked"> + <i + v-if="!editMode" + class="fa fa-pencil" + aria-hidden="true"> + </i> + <span> + {{buttonLabel}} + </span> </button> </template> diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue index fd1a21e15b4..96d6a75bb61 100644 --- a/app/assets/javascripts/repo/components/repo_editor.vue +++ b/app/assets/javascripts/repo/components/repo_editor.vue @@ -8,38 +8,39 @@ const RepoEditor = { data: () => Store, destroyed() { - // this.monacoInstance.getModels().forEach((m) => { - // m.dispose(); - // }); - this.monacoInstance.destroy(); + if (Helper.monacoInstance) { + Helper.monacoInstance.destroy(); + } }, mounted() { Service.getRaw(this.activeFile.raw_path) - .then((rawResponse) => { - Store.blobRaw = rawResponse.data; - Helper.findOpenedFileFromActive().plain = rawResponse.data; + .then((rawResponse) => { + Store.blobRaw = rawResponse.data; + Store.activeFile.plain = rawResponse.data; - const monacoInstance = this.monaco.editor.create(this.$el, { - model: null, - readOnly: false, - contextmenu: false, - }); + const monacoInstance = Helper.monaco.editor.create(this.$el, { + model: null, + readOnly: false, + contextmenu: false, + }); - Store.monacoInstance = monacoInstance; + Helper.monacoInstance = monacoInstance; - this.addMonacoEvents(); + this.addMonacoEvents(); - const languages = this.monaco.languages.getLanguages(); - const languageID = Helper.getLanguageIDForFile(this.activeFile, languages); - this.showHide(); - const newModel = this.monaco.editor.createModel(this.blobRaw, languageID); - - this.monacoInstance.setModel(newModel); - }).catch(Helper.loadingError); + this.setupEditor(); + }) + .catch(Helper.loadingError); }, methods: { + setupEditor() { + this.showHide(); + + Helper.setMonacoModelFromLanguage(); + }, + showHide() { if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) { this.$el.style.display = 'none'; @@ -49,41 +50,36 @@ const RepoEditor = { }, addMonacoEvents() { - this.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp); - this.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this)); + Helper.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp); + Helper.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this)); }, onMonacoEditorKeysPressed() { - Store.setActiveFileContents(this.monacoInstance.getValue()); + Store.setActiveFileContents(Helper.monacoInstance.getValue()); }, onMonacoEditorMouseUp(e) { + if (!e.target.position) return; const lineNumber = e.target.position.lineNumber; - if (e.target.element.className === 'line-numbers') { + if (e.target.element.classList.contains('line-numbers')) { location.hash = `L${lineNumber}`; Store.activeLine = lineNumber; + + Helper.monacoInstance.setPosition({ + lineNumber: this.activeLine, + column: 1, + }); } }, }, watch: { - activeLine() { - this.monacoInstance.setPosition({ - lineNumber: this.activeLine, - column: 1, - }); - }, - - activeFileLabel() { - this.showHide(); - }, - dialog: { handler(obj) { const newObj = obj; if (newObj.status) { newObj.status = false; - this.openedFiles.map((file) => { + this.openedFiles = this.openedFiles.map((file) => { const f = file; if (f.active) { this.blobRaw = f.plain; @@ -94,35 +90,21 @@ const RepoEditor = { return f; }); this.editMode = false; + Store.toggleBlobView(); } }, deep: true, }, - isTree() { - this.showHide(); - }, - - openedFiles() { - this.showHide(); - }, - - binary() { - this.showHide(); - }, - blobRaw() { - this.showHide(); - - if (this.isTree) return; - - this.monacoInstance.setModel(null); - - const languages = this.monaco.languages.getLanguages(); - const languageID = Helper.getLanguageIDForFile(this.activeFile, languages); - const newModel = this.monaco.editor.createModel(this.blobRaw, languageID); - - this.monacoInstance.setModel(newModel); + if (Helper.monacoInstance && !this.isTree) { + this.setupEditor(); + } + }, + }, + computed: { + shouldHideEditor() { + return !this.openedFiles.length || (this.binary && !this.activeFile.raw); }, }, }; @@ -131,5 +113,5 @@ export default RepoEditor; </script> <template> -<div id="ide"></div> +<div id="ide" v-if='!shouldHideEditor'></div> </template> diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/repo/components/repo_file.vue index f604bc22a26..20ebf840774 100644 --- a/app/assets/javascripts/repo/components/repo_file.vue +++ b/app/assets/javascripts/repo/components/repo_file.vue @@ -33,6 +33,26 @@ const RepoFile = { canShowFile() { return !this.loading.tree || this.hasFiles; }, + + fileIcon() { + const classObj = { + 'fa-spinner fa-spin': this.file.loading, + [this.file.icon]: !this.file.loading, + }; + return classObj; + }, + + fileIndentation() { + return { + 'margin-left': `${this.file.level * 10}px`, + }; + }, + + activeFileClass() { + return { + active: this.activeFile.url === this.file.url, + }; + }, }, methods: { @@ -46,21 +66,42 @@ export default RepoFile; </script> <template> -<tr class="file" v-if="canShowFile" :class="{'active': activeFile.url === file.url}"> - <td @click.prevent="linkClicked(file)"> - <i class="fa file-icon" v-if="!file.loading" :class="file.icon" :style="{'margin-left': file.level * 10 + 'px'}"></i> - <i class="fa fa-spinner fa-spin" v-if="file.loading" :style="{'margin-left': file.level * 10 + 'px'}"></i> - <a :href="file.url" class="repo-file-name" :title="file.url">{{file.name}}</a> +<tr + v-if="canShowFile" + class="file" + :class="activeFileClass" + @click.prevent="linkClicked(file)"> + <td> + <i + class="fa fa-fw file-icon" + :class="fileIcon" + :style="fileIndentation" + aria-label="file icon"> + </i> + <a + :href="file.url" + class="repo-file-name" + :title="file.url"> + {{file.name}} + </a> </td> - <td v-if="!isMini" class="hidden-sm hidden-xs"> - <div class="commit-message"> - <a :href="file.lastCommitUrl">{{file.lastCommitMessage}}</a> - </div> - </td> + <template v-if="!isMini"> + <td class="hidden-sm hidden-xs"> + <div class="commit-message"> + <a @click.stop :href="file.lastCommitUrl"> + {{file.lastCommitMessage}} + </a> + </div> + </td> - <td v-if="!isMini" class="hidden-xs"> - <span class="commit-update" :title="tooltipTitle(file.lastCommitUpdate)">{{timeFormated(file.lastCommitUpdate)}}</span> - </td> + <td class="hidden-xs"> + <span + class="commit-update" + :title="tooltipTitle(file.lastCommitUpdate)"> + {{timeFormated(file.lastCommitUpdate)}} + </span> + </td> + </template> </tr> </template> diff --git a/app/assets/javascripts/repo/components/repo_file_buttons.vue b/app/assets/javascripts/repo/components/repo_file_buttons.vue index 628d02ca704..e43ef366f47 100644 --- a/app/assets/javascripts/repo/components/repo_file_buttons.vue +++ b/app/assets/javascripts/repo/components/repo_file_buttons.vue @@ -15,7 +15,7 @@ const RepoFileButtons = { }, canPreview() { - return Helper.isKindaBinary(); + return Helper.isRenderable(); }, }, @@ -28,15 +28,42 @@ export default RepoFileButtons; </script> <template> -<div id="repo-file-buttons" v-if="isMini"> - <a :href="activeFile.raw_path" target="_blank" class="btn btn-default raw" rel="noopener noreferrer">{{rawDownloadButtonLabel}}</a> + <div id="repo-file-buttons"> + <a + :href="activeFile.raw_path" + target="_blank" + class="btn btn-default raw" + rel="noopener noreferrer"> + {{rawDownloadButtonLabel}} + </a> - <div class="btn-group" role="group" aria-label="File actions"> - <a :href="activeFile.blame_path" class="btn btn-default blame">Blame</a> - <a :href="activeFile.commits_path" class="btn btn-default history">History</a> - <a :href="activeFile.permalink" class="btn btn-default permalink">Permalink</a> - </div> + <div + class="btn-group" + role="group" + aria-label="File actions"> + <a + :href="activeFile.blame_path" + class="btn btn-default blame"> + Blame + </a> + <a + :href="activeFile.commits_path" + class="btn btn-default history"> + History + </a> + <a + :href="activeFile.permalink" + class="btn btn-default permalink"> + Permalink + </a> + </div> - <a href="#" v-if="canPreview" @click.prevent="rawPreviewToggle" class="btn btn-default preview">{{activeFileLabel}}</a> -</div> + <a + v-if="canPreview" + href="#" + @click.prevent="rawPreviewToggle" + class="btn btn-default preview"> + {{activeFileLabel}} + </a> + </div> </template> diff --git a/app/assets/javascripts/repo/components/repo_file_options.vue b/app/assets/javascripts/repo/components/repo_file_options.vue index ba53ce0eecc..6a15755f029 100644 --- a/app/assets/javascripts/repo/components/repo_file_options.vue +++ b/app/assets/javascripts/repo/components/repo_file_options.vue @@ -17,7 +17,7 @@ export default RepoFileOptions; </script> <template> -<tr v-if="isMini" class="repo-file-options"> + <tr v-if="isMini" class="repo-file-options"> <td> <span class="title">{{projectName}}</span> </td> diff --git a/app/assets/javascripts/repo/components/repo_loading_file.vue b/app/assets/javascripts/repo/components/repo_loading_file.vue index 38e9f16d041..bc8c64c8362 100644 --- a/app/assets/javascripts/repo/components/repo_loading_file.vue +++ b/app/assets/javascripts/repo/components/repo_loading_file.vue @@ -18,9 +18,15 @@ const RepoLoadingFile = { }, }, + computed: { + showGhostLines() { + return this.loading.tree && !this.hasFiles; + }, + }, + methods: { lineOfCode(n) { - return `line-of-code-${n}`; + return `skeleton-line-${n}`; }, }, }; @@ -29,23 +35,42 @@ export default RepoLoadingFile; </script> <template> -<tr v-if="loading.tree && !hasFiles" class="loading-file"> - <td> - <div class="animation-container animation-container-small"> - <div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div> - </div> - </td> + <tr + v-if="showGhostLines" + class="loading-file"> + <td> + <div + class="animation-container animation-container-small"> + <div + v-for="n in 6" + :key="n" + :class="lineOfCode(n)"> + </div> + </div> + </td> - <td v-if="!isMini" class="hidden-sm hidden-xs"> - <div class="animation-container"> - <div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div> - </div> - </td> + <td + v-if="!isMini" + class="hidden-sm hidden-xs"> + <div class="animation-container"> + <div + v-for="n in 6" + :key="n" + :class="lineOfCode(n)"> + </div> + </div> + </td> - <td v-if="!isMini" class="hidden-xs"> - <div class="animation-container animation-container-small"> - <div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div> - </div> - </td> -</tr> + <td + v-if="!isMini" + class="hidden-xs"> + <div class="animation-container animation-container-small"> + <div + v-for="n in 6" + :key="n" + :class="lineOfCode(n)"> + </div> + </div> + </td> + </tr> </template> diff --git a/app/assets/javascripts/repo/components/repo_prev_directory.vue b/app/assets/javascripts/repo/components/repo_prev_directory.vue index 6a0d684052f..bbdbdc61e38 100644 --- a/app/assets/javascripts/repo/components/repo_prev_directory.vue +++ b/app/assets/javascripts/repo/components/repo_prev_directory.vue @@ -1,4 +1,6 @@ <script> +import RepoMixin from '../mixins/repo_mixin'; + const RepoPreviousDirectory = { props: { prevUrl: { @@ -7,6 +9,14 @@ const RepoPreviousDirectory = { }, }, + mixins: [RepoMixin], + + computed: { + colSpanCondition() { + return this.isMini ? undefined : 3; + }, + }, + methods: { linkClicked(file) { this.$emit('linkclicked', file); @@ -19,8 +29,10 @@ export default RepoPreviousDirectory; <template> <tr class="prev-directory"> - <td colspan="3"> - <a :href="prevUrl" @click.prevent="linkClicked(prevUrl)">..</a> + <td + :colspan="colSpanCondition" + @click.prevent="linkClicked(prevUrl)"> + <a :href="prevUrl">..</a> </td> </tr> </template> diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue index 0d4f8c6635e..72b40288566 100644 --- a/app/assets/javascripts/repo/components/repo_sidebar.vue +++ b/app/assets/javascripts/repo/components/repo_sidebar.vue @@ -8,7 +8,7 @@ import RepoFile from './repo_file.vue'; import RepoLoadingFile from './repo_loading_file.vue'; import RepoMixin from '../mixins/repo_mixin'; -const RepoSidebar = { +export default { mixins: [RepoMixin], components: { 'repo-file-options': RepoFileOptions, @@ -35,7 +35,7 @@ const RepoSidebar = { fileClicked(clickedFile) { let file = clickedFile; - + if (file.loading) return; file.loading = true; if (file.type === 'tree' && file.opened) { file = Store.removeChildFilesOfTree(file); @@ -59,12 +59,10 @@ const RepoSidebar = { }, }, }; - -export default RepoSidebar; </script> <template> -<div id="sidebar" :class="{'sidebar-mini' : isMini}" v-cloak> +<div id="sidebar" :class="{'sidebar-mini' : isMini}"> <table class="table"> <thead v-if="!isMini"> <tr> diff --git a/app/assets/javascripts/repo/components/repo_tab.vue b/app/assets/javascripts/repo/components/repo_tab.vue index fc66a8ea953..0d0c34ec741 100644 --- a/app/assets/javascripts/repo/components/repo_tab.vue +++ b/app/assets/javascripts/repo/components/repo_tab.vue @@ -18,8 +18,8 @@ const RepoTab = { }, changedClass() { const tabChangedObj = { - 'fa-times': !this.tab.changed, - 'fa-circle': this.tab.changed, + 'fa-times close-icon': !this.tab.changed, + 'fa-circle unsaved-icon': this.tab.changed, }; return tabChangedObj; }, @@ -28,9 +28,9 @@ const RepoTab = { methods: { tabClicked: Store.setActiveFiles, - xClicked(file) { + closeTab(file) { if (file.changed) return; - this.$emit('xclicked', file); + this.$emit('tabclosed', file); }, }, }; @@ -39,11 +39,11 @@ export default RepoTab; </script> <template> -<li> +<li @click="tabClicked(tab)"> <a href="#0" class="close" - @click.prevent="xClicked(tab)" + @click.stop.prevent="closeTab(tab)" :aria-label="closeLabel"> <i class="fa" diff --git a/app/assets/javascripts/repo/components/repo_tabs.vue b/app/assets/javascripts/repo/components/repo_tabs.vue index bbd60d9d793..9c5bfc5d0cf 100644 --- a/app/assets/javascripts/repo/components/repo_tabs.vue +++ b/app/assets/javascripts/repo/components/repo_tabs.vue @@ -13,7 +13,7 @@ const RepoTabs = { data: () => Store, methods: { - xClicked(file) { + tabClosed(file) { Store.removeFromOpenedFiles(file); }, }, @@ -23,10 +23,14 @@ export default RepoTabs; </script> <template> -<ul - v-if="isMini" - id="tabs"> - <repo-tab v-for="tab in openedFiles" :key="tab.id" :tab="tab" :class="{'active' : tab.active}" @xclicked="xClicked"/> +<ul id="tabs"> + <repo-tab + v-for="tab in openedFiles" + :key="tab.id" + :tab="tab" + :class="{'active' : tab.active}" + @tabclosed="tabClosed" + /> <li class="tabs-divider" /> </ul> </template> diff --git a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js b/app/assets/javascripts/repo/helpers/monaco_loader_helper.js index c1a0e80f8f3..f8729bbf585 100644 --- a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js +++ b/app/assets/javascripts/repo/helpers/monaco_loader_helper.js @@ -1,13 +1,14 @@ /* global monaco */ import RepoEditor from '../components/repo_editor.vue'; import Store from '../stores/repo_store'; +import Helper from '../helpers/repo_helper'; import monacoLoader from '../monaco_loader'; function repoEditorLoader() { Store.monacoLoading = true; return new Promise((resolve, reject) => { monacoLoader(['vs/editor/editor.main'], () => { - Store.monaco = monaco; + Helper.monaco = monaco; Store.monacoLoading = false; resolve(RepoEditor); }, () => { diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js index 17aaa0e1584..2bd8d7eea65 100644 --- a/app/assets/javascripts/repo/helpers/repo_helper.js +++ b/app/assets/javascripts/repo/helpers/repo_helper.js @@ -4,6 +4,8 @@ import Store from '../stores/repo_store'; import '../../flash'; const RepoHelper = { + monacoInstance: null, + getDefaultActiveFile() { return { active: true, @@ -37,10 +39,6 @@ const RepoHelper = { return fileName.split('.').pop(); }, - getBranch() { - return $('button.dropdown-menu-toggle').attr('data-ref'); - }, - getLanguageIDForFile(file, langs) { const ext = RepoHelper.getFileExtension(file.name); const foundLang = RepoHelper.findLanguage(ext, langs); @@ -48,8 +46,12 @@ const RepoHelper = { return foundLang ? foundLang.id : 'plaintext'; }, - getFilePathFromFullPath(fullPath, branch) { - return fullPath.split(`${Store.projectUrl}/blob/${branch}`)[1]; + setMonacoModelFromLanguage() { + RepoHelper.monacoInstance.setModel(null); + const languages = RepoHelper.monaco.languages.getLanguages(); + const languageID = RepoHelper.getLanguageIDForFile(Store.activeFile, languages); + const newModel = RepoHelper.monaco.editor.createModel(Store.blobRaw, languageID); + RepoHelper.monacoInstance.setModel(newModel); }, findLanguage(ext, langs) { @@ -62,11 +64,11 @@ const RepoHelper = { file.opened = true; file.icon = 'fa-folder-open'; - RepoHelper.toURL(file.url, file.name); + RepoHelper.updateHistoryEntry(file.url, file.name); return file; }, - isKindaBinary() { + isRenderable() { const okExts = ['md', 'svg']; return okExts.indexOf(Store.activeFile.extension) > -1; }, @@ -80,22 +82,8 @@ const RepoHelper = { .catch(RepoHelper.loadingError); }, - toggleFakeTab(loading, file) { - if (loading) return Store.addPlaceholderFile(); - return Store.removeFromOpenedFiles(file); - }, - - setLoading(loading, file) { - if (Service.url.indexOf('blob') > -1) { - Store.loading.blob = loading; - return RepoHelper.toggleFakeTab(loading, file); - } - - if (Service.url.indexOf('tree') > -1) Store.loading.tree = loading; - - return undefined; - }, - + // when you open a directory you need to put the directory files under + // the directory... This will merge the list of the current directory and the new list. getNewMergedList(inDirectory, currentList, newList) { const newListSorted = newList.sort(this.compareFilesCaseInsensitive); if (!inDirectory) return newListSorted; @@ -104,6 +92,9 @@ const RepoHelper = { return RepoHelper.mergeNewListToOldList(newListSorted, currentList, inDirectory, indexOfFile); }, + // within the get new merged list this does the merging of the current list of files + // and the new list of files. The files are never "in" another directory they just + // appear like they are because of the margin. mergeNewListToOldList(newList, oldList, inDirectory, indexOfFile) { newList.reverse().forEach((newFile) => { const fileIndex = indexOfFile + 1; @@ -141,11 +132,9 @@ const RepoHelper = { getContent(treeOrFile) { let file = treeOrFile; - // const loadingData = RepoHelper.setLoading(true); return Service.getContent() .then((response) => { const data = response.data; - // RepoHelper.setLoading(false, loadingData); Store.isTree = RepoHelper.isTree(data); if (!Store.isTree) { if (!file) file = data; @@ -246,37 +235,19 @@ const RepoHelper = { }, dataToListOfFiles(data) { - const a = []; - - // push in blobs - data.blobs.forEach((blob) => { - a.push(RepoHelper.serializeBlob(blob)); - }); - - data.trees.forEach((tree) => { - a.push(RepoHelper.serializeTree(tree)); - }); - - data.submodules.forEach((submodule) => { - a.push(RepoHelper.serializeSubmodule(submodule)); - }); - - return a; + const { blobs, trees, submodules } = data; + return [ + ...blobs.map(blob => RepoHelper.serializeBlob(blob)), + ...trees.map(tree => RepoHelper.serializeTree(tree)), + ...submodules.map(submodule => RepoHelper.serializeSubmodule(submodule)), + ]; }, genKey() { return RepoHelper.Time.now().toFixed(3); }, - getStateKey() { - return RepoHelper.key; - }, - - setStateKey(key) { - RepoHelper.key = key; - }, - - toURL(url, title) { + updateHistoryEntry(url, title) { const history = window.history; RepoHelper.key = RepoHelper.genKey(); @@ -293,7 +264,7 @@ const RepoHelper = { }, loadingError() { - Flash('Unable to load the file at this time.'); + Flash('Unable to load this content at this time.'); }, }; diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js index 3e37da1726e..6c1d468e937 100644 --- a/app/assets/javascripts/repo/index.js +++ b/app/assets/javascripts/repo/index.js @@ -33,6 +33,8 @@ function setInitialStore(data) { Store.projectId = data.projectId; Store.projectName = data.projectName; Store.projectUrl = data.projectUrl; + Store.canCommit = data.canCommit; + Store.onTopOfBranch = data.onTopOfBranch; Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref'); Store.checkIsCommitable(); } @@ -43,6 +45,9 @@ function initRepo(el) { components: { repo: Repo, }, + render(createElement) { + return createElement('repo'); + }, }); } diff --git a/app/assets/javascripts/repo/services/repo_service.js b/app/assets/javascripts/repo/services/repo_service.js index 17578f3bbf3..3cf204e6ec8 100644 --- a/app/assets/javascripts/repo/services/repo_service.js +++ b/app/assets/javascripts/repo/services/repo_service.js @@ -13,14 +13,6 @@ const RepoService = { }, richExtensionRegExp: /md/, - checkCurrentBranchIsCommitable() { - const url = Store.service.refsUrl; - return axios.get(url, { params: { - ref: Store.currentBranch, - search: Store.currentBranch, - } }); - }, - getRaw(url) { return axios.get(url, { // Stop Axios from parsing a JSON file into a JS object @@ -75,7 +67,11 @@ const RepoService = { commitFiles(payload, cb) { Api.commitMultiple(Store.projectId, payload, (data) => { - 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(); }); }, diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js index bb605540aad..1c0df528aea 100644 --- a/app/assets/javascripts/repo/stores/repo_store.js +++ b/app/assets/javascripts/repo/stores/repo_store.js @@ -5,8 +5,9 @@ import Service from '../services/repo_service'; const RepoStore = { monaco: {}, monacoLoading: false, - monacoInstance: {}, service: '', + canCommit: false, + onTopOfBranch: false, editMode: false, isTree: false, isRoot: false, @@ -52,14 +53,7 @@ const RepoStore = { // mutations checkIsCommitable() { - RepoStore.service.checkCurrentBranchIsCommitable() - .then((data) => { - // you shouldn't be able to make commits on commits or tags. - const { Branches, Commits, Tags } = data.data; - if (Branches && Branches.length) RepoStore.isCommitable = true; - if (Commits && Commits.length) RepoStore.isCommitable = false; - if (Tags && Tags.length) RepoStore.isCommitable = false; - }).catch(() => Flash('Failed to check if branch can be committed to.')); + RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit; }, addFilesToDirectory(inDirectory, currentList, newList) { @@ -90,7 +84,7 @@ const RepoStore = { }).catch(Helper.loadingError); } - if (!file.loading) Helper.toURL(file.url, file.name); + if (!file.loading) Helper.updateHistoryEntry(file.url, file.name); RepoStore.binary = file.binary; }, @@ -117,15 +111,15 @@ const RepoStore = { removeChildFilesOfTree(tree) { let foundTree = false; const treeToClose = tree; - let wereDone = false; + let canStopSearching = false; RepoStore.files = RepoStore.files.filter((file) => { const isItTheTreeWeWant = file.url === treeToClose.url; // if it's the next tree if (foundTree && file.type === 'tree' && !isItTheTreeWeWant && file.level === treeToClose.level) { - wereDone = true; + canStopSearching = true; return true; } - if (wereDone) return true; + if (canStopSearching) return true; if (isItTheTreeWeWant) foundTree = true; @@ -142,8 +136,8 @@ const RepoStore = { if (file.type === 'tree') return; let foundIndex; RepoStore.openedFiles = RepoStore.openedFiles.filter((openedFile, i) => { - if (openedFile.url === file.url) foundIndex = i; - return openedFile.url !== file.url; + if (openedFile.path === file.path) foundIndex = i; + return openedFile.path !== file.path; }); // now activate the right tab based on what you closed. @@ -157,36 +151,16 @@ const RepoStore = { return; } - if (foundIndex) { - if (foundIndex > 0) { - RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]); - } + if (foundIndex && foundIndex > 0) { + RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]); } }, - addPlaceholderFile() { - const randomURL = Helper.Time.now(); - const newFakeFile = { - active: false, - binary: true, - type: 'blob', - loading: true, - mime_type: 'loading', - name: 'loading', - url: randomURL, - fake: true, - }; - - RepoStore.openedFiles.push(newFakeFile); - - return newFakeFile; - }, - addToOpenedFiles(file) { const openFile = file; const openedFilesAlreadyExists = RepoStore.openedFiles - .some(openedFile => openedFile.url === openFile.url); + .some(openedFile => openedFile.path === openFile.path); if (openedFilesAlreadyExists) return; diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue index cfacba09fad..8e7abdbffef 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -71,7 +71,7 @@ export default { /> <div v-if="!isConfidential" class="no-value confidential-value"> <i class="fa fa-eye is-not-confidential"></i> - This issue is not confidential + Not confidential </div> <div v-else class="value confidential-value hide-collapsed"> <i aria-hidden="true" data-hidden="true" class="fa fa-eye-slash is-confidential"></i> diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/popup_dialog.vue index 7d339c0e753..994b33bc1c9 100644 --- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue +++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue @@ -1,31 +1,37 @@ <script> -const PopupDialog = { +export default { name: 'popup-dialog', props: { - open: Boolean, - title: String, - body: String, + title: { + type: String, + required: true, + }, + body: { + type: String, + required: true, + }, kind: { type: String, + required: false, default: 'primary', }, closeButtonLabel: { type: String, + required: false, default: 'Cancel', }, primaryButtonLabel: { type: String, - default: 'Save changes', + required: true, }, }, computed: { - typeOfClass() { - const className = `btn-${this.kind}`; - const returnObj = {}; - returnObj[className] = true; - return returnObj; + btnKindClass() { + return { + [`btn-${this.kind}`]: true, + }; }, }, @@ -33,33 +39,45 @@ const PopupDialog = { close() { this.$emit('toggle', false); }, - - yesClick() { - this.$emit('submit', true); - }, - - noClick() { - this.$emit('submit', false); + emitSubmit(status) { + this.$emit('submit', status); }, }, }; - -export default PopupDialog; </script> + <template> -<div class="modal popup-dialog" tabindex="-1" v-show="open" role="dialog"> +<div + class="modal popup-dialog" + role="dialog" + tabindex="-1"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> - <button type="button" class="close" @click="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> + <button type="button" + class="close" + @click="close" + aria-label="Close"> + <span aria-hidden="true">×</span> + </button> <h4 class="modal-title">{{this.title}}</h4> </div> <div class="modal-body"> <p>{{this.body}}</p> </div> <div class="modal-footer"> - <button type="button" class="btn btn-default" data-dismiss="modal" @click="noClick">{{closeButtonLabel}}</button> - <button type="button" class="btn" :class="typeOfClass" @click="yesClick">{{primaryButtonLabel}}</button> + <button + type="button" + class="btn btn-default" + @click="emitSubmit(false)"> + {{closeButtonLabel}} + </button> + <button type="button" + class="btn" + :class="btnKindClass" + @click="emitSubmit(true)"> + {{primaryButtonLabel}} + </button> </div> </div> </div> diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 3cd7f81da47..667b73e150d 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -187,3 +187,81 @@ a { .fade-in-full { animation: fadeInFull $fade-in-duration 1; } + + +.animation-container { + background: $repo-editor-grey; + height: 40px; + overflow: hidden; + position: relative; + + &.animation-container-small { + height: 12px; + } + + &::before { + animation-duration: 1s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: blockTextShine; + animation-timing-function: linear; + background-image: $repo-editor-linear-gradient; + background-repeat: no-repeat; + background-size: 800px 45px; + content: ' '; + display: block; + height: 100%; + position: relative; + } + + div { + background: $white-light; + height: 6px; + left: 0; + position: absolute; + right: 0; + } + + .skeleton-line-1 { + left: 0; + top: 8px; + } + + .skeleton-line-2 { + left: 150px; + top: 0; + height: 10px; + } + + .skeleton-line-3 { + left: 0; + top: 23px; + } + + .skeleton-line-4 { + left: 0; + top: 38px; + } + + .skeleton-line-5 { + left: 200px; + top: 28px; + height: 10px; + } + + .skeleton-line-6 { + top: 14px; + left: 230px; + height: 10px; + } +} + +@keyframes blockTextShine { + 0% { + transform: translateX(-468px); + } + + 100% { + transform: translateX(468px); + } +} diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index bd0367f86dd..bd521028c44 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -117,10 +117,6 @@ body { margin-top: $header-height + $performance-bar-height; } -[v-cloak] { - display: none; -} - .vertical-center { min-height: 100vh; display: flex; diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index fcd4c72b430..e3920b5d3d9 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -204,6 +204,16 @@ } } + div.avatar { + display: inline-flex; + justify-content: center; + align-items: center; + + .center { + line-height: 14px; + } + } + strong { color: $gl-text-color; } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index d386ac5ba9c..071f20fc457 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -161,6 +161,8 @@ } .nav-controls { + @include new-style-dropdown; + display: inline-block; float: right; text-align: right; diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss index 795ee91af8b..3e2f23e6b2a 100644 --- a/app/assets/stylesheets/new_nav.scss +++ b/app/assets/stylesheets/new_nav.scss @@ -403,6 +403,7 @@ header.navbar-gitlab-new { } .breadcrumbs-extra { + display: flex; flex: 0 0 auto; margin-left: auto; } diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss index faedd207e01..840a4f07a34 100644 --- a/app/assets/stylesheets/new_sidebar.scss +++ b/app/assets/stylesheets/new_sidebar.scss @@ -97,13 +97,12 @@ $new-sidebar-collapsed-width: 50px; top: $header-height; bottom: 0; left: 0; - overflow: auto; background-color: $gray-normal; box-shadow: inset -2px 0 0 $border-color; + transform: translate3d(0, 0, 0); &.sidebar-icons-only { width: $new-sidebar-collapsed-width; - overflow-x: hidden; .badge, .project-title { @@ -111,7 +110,11 @@ $new-sidebar-collapsed-width: 50px; } .nav-item-name { - opacity: 0; + display: none; + } + + .sidebar-top-level-items > li > a { + min-height: 44px; } } @@ -176,6 +179,12 @@ $new-sidebar-collapsed-width: 50px; } } +.nav-sidebar-inner-scroll { + height: 100%; + width: 100%; + overflow: auto; +} + .with-performance-bar .nav-sidebar { top: $header-height + $performance-bar-height; } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 215bedc04fd..913a1a95dca 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -560,9 +560,13 @@ } .diff-files-changed { + .inline-parallel-buttons { + position: relative; + z-index: 1; + } + .commit-stat-summary { @include new-style-dropdown; - z-index: -1; @media (min-width: $screen-sm-min) { margin-left: -$gl-padding; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index d14b976374c..49839a9b528 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -8,13 +8,13 @@ .is-confidential { color: $orange-600; background-color: $orange-50; - border-radius: 3px; + border-radius: $border-radius-default; padding: 5px; margin: 0 3px 0 -4px; } .is-not-confidential { - border-radius: 3px; + border-radius: $border-radius-default; padding: 5px; margin: 0 3px 0 -4px; } @@ -81,6 +81,7 @@ border: 1px solid $white-normal; padding: 5px; max-height: calc(100vh - 100px); + max-width: 100%; } .emoji-block { @@ -259,7 +260,7 @@ padding-top: 10px; } - &:not(.issue-boards-sidebar):not([data-signed-in]) { + &:not(.issue-boards-sidebar):not([data-signed-in]):not([data-always-show-toggle]) { .issuable-sidebar-header { display: none; } diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index ad17078c98a..1f4d4698199 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -1,6 +1,6 @@ .fade-enter-active, .fade-leave-active { - transition: opacity .5s; + transition: opacity $sidebar-transition-duration; } .monaco-loader { @@ -28,11 +28,6 @@ .project-refs-form, .project-refs-target-form { display: inline-block; - - &.disabled { - opacity: 0.5; - pointer-events: none; - } } .fade-enter, @@ -52,14 +47,26 @@ margin: 20px; } -.repository-view.tree-content-holder { +.repository-view { border: 1px solid $border-color; border-radius: $border-radius-default; color: $almost-black; + .tree-content-holder { + display: flex; + max-height: 100vh; + min-height: 300px; + } + + .tree-content-holder-mini { + height: 100vh; + } + .panel-right { - display: inline-block; + display: flex; + flex-direction: column; width: 80%; + height: 100%; .monaco-editor.vs { .line-numbers { @@ -90,16 +97,17 @@ } .blob-viewer-container { - height: calc(100vh - 63px); + flex: 1; overflow: auto; } #tabs { + flex-shrink: 0; + display: flex; + width: 100%; padding-left: 0; margin-bottom: 0; - display: flex; white-space: nowrap; - width: 100%; overflow-y: hidden; overflow-x: auto; @@ -114,6 +122,7 @@ border-right: 1px solid $white-dark; border-bottom: 1px solid $white-dark; white-space: nowrap; + cursor: pointer; &.remove { animation: swipeRightDissapear ease-in 0.1s; @@ -133,10 +142,10 @@ a { @include str-truncated(100px); color: $black; - display: inline-block; width: 100px; text-align: center; vertical-align: middle; + text-decoration: none; &.close { width: auto; @@ -146,15 +155,15 @@ } } - i.fa.fa-times, - i.fa.fa-circle { + .close-icon, + .unsaved-icon { float: right; margin-top: 3px; margin-left: 15px; color: $gray-darkest; } - i.fa.fa-circle { + .unsaved-icon { color: $brand-success; } @@ -204,7 +213,7 @@ background: $gray-light; padding: 20px; - span.help-block { + .help-block { padding-top: 7px; margin-top: 0; } @@ -226,13 +235,12 @@ } #sidebar { + flex: 1; + height: 100%; &.sidebar-mini { - display: inline-block; - vertical-align: top; width: 20%; border-right: 1px solid $white-normal; - height: calc(100vh + 20px); overflow: auto; } @@ -261,7 +269,6 @@ text-transform: uppercase; font-weight: bold; color: $gray-darkest; - width: 185px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -270,7 +277,7 @@ } } - .fa { + .file-icon { margin-right: 5px; } @@ -280,118 +287,22 @@ } a { + @include str-truncated(250px); color: $almost-black; display: inline-block; vertical-align: middle; } - - ul { - list-style-type: none; - padding: 0; - - li { - border-bottom: 1px solid $border-gray-normal; - padding: 10px 20px; - - a { - color: $almost-black; - } - - .fa { - font-size: $code_font_size; - margin-right: 5px; - } - } - } - } - -} - -.animation-container { - background: $repo-editor-grey; - height: 40px; - overflow: hidden; - position: relative; - - &.animation-container-small { - height: 12px; - } - - &::before { - animation-duration: 1s; - animation-fill-mode: forwards; - animation-iteration-count: infinite; - animation-name: blockTextShine; - animation-timing-function: linear; - background-image: $repo-editor-linear-gradient; - background-repeat: no-repeat; - background-size: 800px 45px; - content: ' '; - display: block; - height: 100%; - position: relative; - } - - div { - background: $white-light; - height: 6px; - left: 0; - position: absolute; - right: 0; - } - - .line-of-code-1 { - left: 0; - top: 8px; - } - - .line-of-code-2 { - left: 150px; - top: 0; - height: 10px; - } - - .line-of-code-3 { - left: 0; - top: 23px; - } - - .line-of-code-4 { - left: 0; - top: 38px; - } - - .line-of-code-5 { - left: 200px; - top: 28px; - height: 10px; - } - - .line-of-code-6 { - top: 14px; - left: 230px; - height: 10px; } } .render-error { - min-height: calc(100vh - 63px); + min-height: calc(100vh - 62px); p { width: 100%; } } -@keyframes blockTextShine { - 0% { - transform: translateX(-468px); - } - - 100% { - transform: translateX(468px); - } -} - @keyframes swipeRightAppear { 0% { transform: scaleX(0.00); diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 0c3b68a7ac3..4079072a930 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -10,7 +10,7 @@ module IssuableActions def destroy issuable.destroy destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym - TodoService.new.public_send(destroy_method, issuable, current_user) + TodoService.new.public_send(destroy_method, issuable, current_user) # rubocop:disable GitlabSecurity/PublicSend name = issuable.human_class_name flash[:notice] = "The #{name} was successfully deleted." diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb index c0ac47e363d..96ce686c989 100644 --- a/app/controllers/groups/application_controller.rb +++ b/app/controllers/groups/application_controller.rb @@ -34,7 +34,7 @@ class Groups::ApplicationController < ApplicationController def build_canonical_path(group) params[:group_id] = group.to_param - + url_for(params) end end diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index baa6645e5ce..ab18d86dcae 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -64,7 +64,7 @@ class Import::GithubController < Import::BaseController end def import_enabled? - __send__("#{provider}_import_enabled?") + __send__("#{provider}_import_enabled?") # rubocop:disable GitlabSecurity/PublicSend end def new_import_url diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index b4213574561..7444826a5d1 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -142,13 +142,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController def oauth @oauth ||= request.env['omniauth.auth'] end - + def fail_login error_message = @user.errors.full_messages.to_sentence return redirect_to omniauth_error_path(oauth['provider'], error: error_message) end - + def fail_ldap_login flash[:alert] = 'Access denied for your LDAP account.' diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index a2e8c10857d..2b8f3977e6e 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -198,6 +198,10 @@ class Projects::BlobController < Projects::ApplicationController json = blob_json(@blob) return render_404 unless json + path_segments = @path.split('/') + path_segments.pop + tree_path = path_segments.join('/') + render json: json.merge( path: blob.path, name: blob.name, @@ -212,6 +216,7 @@ class Projects::BlobController < Projects::ApplicationController raw_path: project_raw_path(project, @id), blame_path: project_blame_path(project, @id), commits_path: project_commits_path(project, @id), + tree_path: project_tree_path(project, File.join(@ref, tree_path)), permalink: project_blob_path(project, File.join(@commit.id, @path)) ) end diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb index b69d46f2c41..26f3c114108 100644 --- a/app/controllers/projects/cycle_analytics/events_controller.rb +++ b/app/controllers/projects/cycle_analytics/events_controller.rb @@ -2,7 +2,7 @@ module Projects module CycleAnalytics class EventsController < Projects::ApplicationController include CycleAnalyticsParams - + before_action :authorize_read_cycle_analytics! before_action :authorize_read_build!, only: [:test, :staging] before_action :authorize_read_issue!, only: [:issue, :production] @@ -11,33 +11,33 @@ module Projects def issue render_events(cycle_analytics[:issue].events) end - + def plan render_events(cycle_analytics[:plan].events) end - + def code render_events(cycle_analytics[:code].events) end - + def test options(events_params)[:branch] = events_params[:branch_name] - + render_events(cycle_analytics[:test].events) end - + def review render_events(cycle_analytics[:review].events) end - + def staging render_events(cycle_analytics[:staging].events) end - + def production render_events(cycle_analytics[:production].events) end - + private def render_events(events) @@ -46,14 +46,14 @@ module Projects format.json { render json: { events: events } } end end - + def cycle_analytics @cycle_analytics ||= ::CycleAnalytics.new(project, options(events_params)) end - + def events_params return {} unless params[:events].present? - + params[:events].permit(:start_date, :branch_name) end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 4de814d0ca8..2a3b73577a5 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -218,8 +218,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo if can?(current_user, :read_environment, environment) && environment.has_metrics? metrics_project_environment_deployment_path(environment.project, environment, deployment) end - - metrics_monitoring_url = + + metrics_monitoring_url = if can?(current_user, :read_environment, environment) environment_metrics_path(environment) end diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index dc882b17143..16a74f82d3f 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -89,7 +89,7 @@ class UploadsController < ApplicationController @uploader.retrieve_from_store!(params[:filename]) else - @uploader = @model.send(upload_mount) + @uploader = @model.public_send(upload_mount) # rubocop:disable GitlabSecurity/PublicSend redirect_to @uploader.url unless @uploader.file_storage? end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 69220a1c0f6..72e26b64e60 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -128,10 +128,10 @@ module CommitsHelper # avatar: true will prepend the avatar image # size: size of the avatar image in px def commit_person_link(commit, options = {}) - user = commit.send(options[:source]) + user = commit.public_send(options[:source]) # rubocop:disable GitlabSecurity/PublicSend - source_name = clean(commit.send "#{options[:source]}_name".to_sym) - source_email = clean(commit.send "#{options[:source]}_email".to_sym) + source_name = clean(commit.public_send(:"#{options[:source]}_name")) # rubocop:disable GitlabSecurity/PublicSend + source_email = clean(commit.public_send(:"#{options[:source]}_email")) # rubocop:disable GitlabSecurity/PublicSend person_name = user.try(:name) || source_name diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index a57b5a8fea5..a18ebfb6030 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -5,7 +5,7 @@ module ImportHelper end def provider_project_link(provider, path_with_namespace) - url = __send__("#{provider}_project_url", path_with_namespace) + url = __send__("#{provider}_project_url", path_with_namespace) # rubocop:disable GitlabSecurity/PublicSend link_to path_with_namespace, url, target: '_blank', rel: 'noopener noreferrer' end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 70ea35fab1e..197c90c4081 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -174,7 +174,14 @@ module IssuablesHelper end def assigned_issuables_count(issuable_type) - current_user.public_send("assigned_open_#{issuable_type}_count") + case issuable_type + when :issues + current_user.assigned_open_issues_count + when :merge_requests + current_user.assigned_open_merge_requests_count + else + raise ArgumentError, "invalid issuable `#{issuable_type}`" + end end def issuable_filter_params @@ -298,10 +305,6 @@ module IssuablesHelper cookies[:collapsed_gutter] == 'true' end - def base_issuable_scope(issuable) - issuable.project.send(issuable.class.table_name).send(issuable_state_scope(issuable)) - end - def issuable_state_scope(issuable) if issuable.respond_to?(:merged?) && issuable.merged? :merged diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index f8860bfee99..86666022a2a 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -32,7 +32,18 @@ module MilestonesHelper end def milestone_issues_by_label_count(milestone, label, state:) - milestone.issues.with_label(label.title).send(state).size + issues = milestone.issues.with_label(label.title) + issues = + case state + when :opened + issues.opened + when :closed + issues.closed + else + raise ArgumentError, "invalid milestone state `#{state}`" + end + + issues.size end # Returns count of milestones for different states diff --git a/app/helpers/pipeline_schedules_helper.rb b/app/helpers/pipeline_schedules_helper.rb index fee1edc2a1b..6edaf78de1b 100644 --- a/app/helpers/pipeline_schedules_helper.rb +++ b/app/helpers/pipeline_schedules_helper.rb @@ -1,10 +1,10 @@ module PipelineSchedulesHelper def timezone_data - ActiveSupport::TimeZone.all.map do |timezone| - { - name: timezone.name, - offset: timezone.utc_offset, - identifier: timezone.tzinfo.identifier + ActiveSupport::TimeZone.all.map do |timezone| + { + name: timezone.name, + offset: timezone.utc_offset, + identifier: timezone.tzinfo.identifier } end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 278d394bc03..bee4950e414 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -149,15 +149,16 @@ module ProjectsHelper # Don't show option "everyone with access" if project is private options = project_feature_options + level = @project.project_feature.public_send(field) # rubocop:disable GitlabSecurity/PublicSend + if @project.private? - level = @project.project_feature.send(field) disabled_option = ProjectFeature::ENABLED highest_available_option = ProjectFeature::PRIVATE if level == disabled_option end options = options_for_select( options.invert, - selected: highest_available_option || @project.project_feature.public_send(field), + selected: highest_available_option || level, disabled: disabled_option ) @@ -488,7 +489,7 @@ module ProjectsHelper end def filename_path(project, filename) - if project && blob = project.repository.send(filename) + if project && blob = project.repository.public_send(filename) # rubocop:disable GitlabSecurity/PublicSend project_blob_path( project, tree_join(project.default_branch, blob.name) diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb index 3b175251446..456598b4c28 100644 --- a/app/helpers/version_check_helper.rb +++ b/app/helpers/version_check_helper.rb @@ -2,7 +2,7 @@ module VersionCheckHelper def version_status_badge if Rails.env.production? && current_application_settings.version_check_enabled image_url = VersionCheck.new.url - image_tag image_url, class: 'js-version-status-badge', lazy: false + image_tag image_url, class: 'js-version-status-badge' end end end diff --git a/app/models/blob_viewer/notebook.rb b/app/models/blob_viewer/notebook.rb index 8632b8a9885..e00b47e6c17 100644 --- a/app/models/blob_viewer/notebook.rb +++ b/app/models/blob_viewer/notebook.rb @@ -2,7 +2,7 @@ module BlobViewer class Notebook < Base include Rich include ClientSide - + self.partial_name = 'notebook' self.extensions = %w(ipynb) self.binary = false diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 8be2dee6479..4692fb5644a 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -194,10 +194,7 @@ module Ci # * Maximum length is 63 bytes # * First/Last Character is not a hyphen def ref_slug - ref.to_s - .downcase - .gsub(/[^a-z0-9]/, '-')[0..62] - .gsub(/(\A-+|-+\z)/, '') + Gitlab::Utils.slugify(ref.to_s) end # Variables whose value does not depend on environment diff --git a/app/models/commit.rb b/app/models/commit.rb index 638fddc5d3d..d41c88b4e30 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -200,7 +200,7 @@ class Commit end def method_missing(m, *args, &block) - @raw.send(m, *args, &block) + @raw.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend end def respond_to_missing?(method, include_private = false) @@ -383,6 +383,6 @@ class Commit end def gpg_commit - @gpg_commit ||= Gitlab::Gpg::Commit.new(self) + @gpg_commit ||= Gitlab::Gpg::Commit.for_commit(self) end end diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index 48547a938fc..193e459977a 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -78,7 +78,7 @@ module CacheMarkdownField def cached_html_up_to_date?(markdown_field) html_field = cached_markdown_fields.html_field(markdown_field) - cached = cached_html_for(markdown_field).present? && __send__(markdown_field).present? + cached = cached_html_for(markdown_field).present? && __send__(markdown_field).present? # rubocop:disable GitlabSecurity/PublicSend return false unless cached markdown_changed = attribute_changed?(markdown_field) || false @@ -93,14 +93,14 @@ module CacheMarkdownField end def attribute_invalidated?(attr) - __send__("#{attr}_invalidated?") + __send__("#{attr}_invalidated?") # rubocop:disable GitlabSecurity/PublicSend end def cached_html_for(markdown_field) raise ArgumentError.new("Unknown field: #{field}") unless cached_markdown_fields.markdown_fields.include?(markdown_field) - __send__(cached_markdown_fields.html_field(markdown_field)) + __send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend end included do diff --git a/app/models/concerns/internal_id.rb b/app/models/concerns/internal_id.rb index 67a0adfcd56..a3d0ac8d862 100644 --- a/app/models/concerns/internal_id.rb +++ b/app/models/concerns/internal_id.rb @@ -9,7 +9,7 @@ module InternalId def set_iid if iid.blank? parent = project || group - records = parent.send(self.class.name.tableize) + records = parent.public_send(self.class.name.tableize) # rubocop:disable GitlabSecurity/PublicSend records = records.with_deleted if self.paranoid? max_iid = records.maximum(:iid) diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index c034bf9cbc0..1db6b2d2fa2 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -56,7 +56,7 @@ module Mentionable end self.class.mentionable_attrs.each do |attr, options| - text = __send__(attr) + text = __send__(attr) # rubocop:disable GitlabSecurity/PublicSend options = options.merge( cache_key: [self, attr], author: author, @@ -100,7 +100,7 @@ module Mentionable end self.class.mentionable_attrs.any? do |attr, _| - __send__(attr) =~ reference_pattern + __send__(attr) =~ reference_pattern # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index 4865c0a14b1..ce69fd34ac5 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -82,7 +82,7 @@ module Participable if attr.respond_to?(:call) source.instance_exec(current_user, ext, &attr) else - process << source.__send__(attr) + process << source.__send__(attr) # rubocop:disable GitlabSecurity/PublicSend end end when Enumerable, ActiveRecord::Relation diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index 60734bc6660..cb59b4da3d7 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -32,6 +32,6 @@ module ProjectFeaturesCompatibility build_project_feature unless project_feature access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED - project_feature.send(:write_attribute, field, access_level) + project_feature.__send__(:write_attribute, field, access_level) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb index 10f4be72016..78ac4f324e7 100644 --- a/app/models/concerns/referable.rb +++ b/app/models/concerns/referable.rb @@ -25,6 +25,11 @@ module Referable to_reference(from_project) end + included do + alias_method :non_referable_inspect, :inspect + alias_method :inspect, :referable_inspect + end + def referable_inspect if respond_to?(:id) "#<#{self.class.name} id:#{id} #{to_reference(full: true)}>" @@ -33,10 +38,6 @@ module Referable end end - def inspect - referable_inspect - end - module ClassMethods # The character that prefixes the actual reference identifier # diff --git a/app/models/deploy_keys_project.rb b/app/models/deploy_keys_project.rb index ae8486bd9ac..b37b9bfbdac 100644 --- a/app/models/deploy_keys_project.rb +++ b/app/models/deploy_keys_project.rb @@ -12,7 +12,7 @@ class DeployKeysProject < ActiveRecord::Base def destroy_orphaned_deploy_key return unless self.deploy_key.destroyed_when_orphaned? && self.deploy_key.orphaned? - + self.deploy_key.destroy end end diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb index 1ac0e123ff1..50fb35c77ec 100644 --- a/app/models/gpg_signature.rb +++ b/app/models/gpg_signature.rb @@ -18,4 +18,8 @@ class GpgSignature < ActiveRecord::Base def commit project.commit(commit_sha) end + + def gpg_commit + Gitlab::Gpg::Commit.new(project, commit_sha) + end end diff --git a/app/models/network/commit.rb b/app/models/network/commit.rb index 8417f200e36..9357e55b419 100644 --- a/app/models/network/commit.rb +++ b/app/models/network/commit.rb @@ -12,7 +12,7 @@ module Network end def method_missing(m, *args, &block) - @commit.send(m, *args, &block) + @commit.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend end def space diff --git a/app/models/project.rb b/app/models/project.rb index 0de7da0ddaa..be248bc99e1 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -60,7 +60,7 @@ class Project < ActiveRecord::Base end before_destroy :remove_private_deploy_keys - after_destroy :remove_pages + after_destroy -> { run_after_commit { remove_pages } } # update visibility_level of forks after_update :update_forks_visibility_level @@ -369,7 +369,10 @@ class Project < ActiveRecord::Base state :failed after_transition [:none, :finished, :failed] => :scheduled do |project, _| - project.run_after_commit { add_import_job } + project.run_after_commit do + job_id = add_import_job + update(import_jid: job_id) if job_id + end end after_transition started: :finished do |project, _| @@ -524,17 +527,26 @@ class Project < ActiveRecord::Base def add_import_job job_id = if forked? - RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path, - forked_from_project.full_path, - self.namespace.full_path) + RepositoryForkWorker.perform_async(id, + forked_from_project.repository_storage_path, + forked_from_project.full_path, + self.namespace.full_path) else RepositoryImportWorker.perform_async(self.id) end + log_import_activity(job_id) + + job_id + end + + def log_import_activity(job_id, type: :import) + job_type = type.to_s.capitalize + if job_id - Rails.logger.info "Import job started for #{full_path} with job ID #{job_id}" + Rails.logger.info("#{job_type} job scheduled for #{full_path} with job ID #{job_id}.") else - Rails.logger.error "Import job failed to start for #{full_path}" + Rails.logger.error("#{job_type} job failed to create for #{full_path}.") end end @@ -543,6 +555,7 @@ class Project < ActiveRecord::Base ProjectCacheWorker.perform_async(self.id) end + update(import_error: nil) remove_import_data end @@ -920,14 +933,14 @@ class Project < ActiveRecord::Base end def execute_hooks(data, hooks_scope = :push_hooks) - hooks.send(hooks_scope).each do |hook| + hooks.public_send(hooks_scope).each do |hook| # rubocop:disable GitlabSecurity/PublicSend hook.async_execute(data, hooks_scope.to_s) end end def execute_services(data, hooks_scope = :push_hooks) # Call only service hooks that are active for this scope - services.send(hooks_scope).each do |service| + services.public_send(hooks_scope).each do |service| # rubocop:disable GitlabSecurity/PublicSend service.async_execute(data) end end @@ -1224,6 +1237,9 @@ class Project < ActiveRecord::Base # TODO: what to do here when not using Legacy Storage? Do we still need to rename and delay removal? def remove_pages + # Projects with a missing namespace cannot have their pages removed + return unless namespace + ::Projects::UpdatePagesConfigurationService.new(self).execute # 1. We rename pages to temporary directory @@ -1282,12 +1298,16 @@ class Project < ActiveRecord::Base status.zero? end + def full_path_slug + Gitlab::Utils.slugify(full_path.to_s) + end + def predefined_variables [ { key: 'CI_PROJECT_ID', value: id.to_s, public: true }, { key: 'CI_PROJECT_NAME', value: path, public: true }, { key: 'CI_PROJECT_PATH', value: full_path, public: true }, - { key: 'CI_PROJECT_PATH_SLUG', value: full_path.parameterize, public: true }, + { key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug, public: true }, { key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path, public: true }, { key: 'CI_PROJECT_URL', value: web_url, public: true } ] diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index 6d1a321f651..7b15a5dd04d 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -115,7 +115,7 @@ class ChatNotificationService < Service def get_channel_field(event) field_name = event_channel_name(event) - self.public_send(field_name) + self.public_send(field_name) # rubocop:disable GitlabSecurity/PublicSend end def build_event_channels diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index e3906943ecd..f422e0ea036 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -53,7 +53,7 @@ class HipchatService < Service return unless supported_events.include?(data[:object_kind]) message = create_message(data) return unless message.present? - gate[room].send('GitLab', message, message_options(data)) + gate[room].send('GitLab', message, message_options(data)) # rubocop:disable GitlabSecurity/PublicSend end def test(data) diff --git a/app/models/protectable_dropdown.rb b/app/models/protectable_dropdown.rb index 122fbce257d..c96edc5a259 100644 --- a/app/models/protectable_dropdown.rb +++ b/app/models/protectable_dropdown.rb @@ -1,5 +1,9 @@ class ProtectableDropdown + REF_TYPES = %i[branches tags].freeze + def initialize(project, ref_type) + raise ArgumentError, "invalid ref type `#{ref_type}`" unless ref_type.in?(REF_TYPES) + @project = project @ref_type = ref_type end @@ -16,7 +20,7 @@ class ProtectableDropdown private def refs - @project.repository.public_send(@ref_type) + @project.repository.public_send(@ref_type) # rubocop:disable GitlabSecurity/PublicSend end def ref_names @@ -24,7 +28,7 @@ class ProtectableDropdown end def protections - @project.public_send("protected_#{@ref_type}") + @project.public_send("protected_#{@ref_type}") # rubocop:disable GitlabSecurity/PublicSend end def non_wildcard_protected_ref_names diff --git a/app/models/redirect_route.rb b/app/models/redirect_route.rb index 090fbd61e6f..31de204d824 100644 --- a/app/models/redirect_route.rb +++ b/app/models/redirect_route.rb @@ -14,7 +14,7 @@ class RedirectRoute < ActiveRecord::Base else 'redirect_routes.path = ? OR redirect_routes.path LIKE ?' end - + where(wheres, path, "#{sanitize_sql_like(path)}/%") end end diff --git a/app/models/repository.rb b/app/models/repository.rb index a761302b06b..c1e4fcf94a4 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -48,7 +48,9 @@ class Repository alias_method(original, name) define_method(name) do - cache_method_output(name, fallback: fallback, memoize_only: memoize_only) { __send__(original) } + cache_method_output(name, fallback: fallback, memoize_only: memoize_only) do + __send__(original) # rubocop:disable GitlabSecurity/PublicSend + end end end @@ -443,9 +445,9 @@ class Repository def method_missing(m, *args, &block) if m == :lookup && !block_given? lookup_cache[m] ||= {} - lookup_cache[m][args.join(":")] ||= raw_repository.send(m, *args, &block) + lookup_cache[m][args.join(":")] ||= raw_repository.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend else - raw_repository.send(m, *args, &block) + raw_repository.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend end end @@ -776,7 +778,7 @@ class Repository end actions.each do |options| - index.public_send(options.delete(:action), options) + index.public_send(options.delete(:action), options) # rubocop:disable GitlabSecurity/PublicSend end options = { diff --git a/app/models/user.rb b/app/models/user.rb index 2b25736bb26..02c3ab6654b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -47,11 +47,6 @@ class User < ActiveRecord::Base devise :lockable, :recoverable, :rememberable, :trackable, :validatable, :omniauthable, :confirmable, :registerable - # devise overrides #inspect, so we manually use the Referable one - def inspect - referable_inspect - end - # Override Devise::Models::Trackable#update_tracked_fields! # to limit database writes to at most once every hour def update_tracked_fields!(request) @@ -1070,7 +1065,7 @@ class User < ActiveRecord::Base # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration def send_devise_notification(notification, *args) return true unless can?(:receive_notifications) - devise_mailer.send(notification, self, *args).deliver_later + devise_mailer.__send__(notification, self, *args).deliver_later # rubocop:disable GitlabSecurity/PublicSend end # This works around a bug in Devise 4.2.0 that erroneously causes a user to diff --git a/app/serializers/project_entity.rb b/app/serializers/project_entity.rb index dc283ba3e7a..b3e5fd21e97 100644 --- a/app/serializers/project_entity.rb +++ b/app/serializers/project_entity.rb @@ -1,6 +1,6 @@ class ProjectEntity < Grape::Entity include RequestAwareEntity - + expose :id expose :name diff --git a/app/serializers/tree_root_entity.rb b/app/serializers/tree_root_entity.rb index 23b65aa4a4c..69702ae1493 100644 --- a/app/serializers/tree_root_entity.rb +++ b/app/serializers/tree_root_entity.rb @@ -1,8 +1,21 @@ # TODO: Inherit from TreeEntity, when `Tree` implements `id` and `name` like `Gitlab::Git::Tree`. class TreeRootEntity < Grape::Entity + include RequestAwareEntity + expose :path - + expose :trees, using: TreeEntity expose :blobs, using: BlobEntity expose :submodules, using: SubmoduleEntity + + expose :parent_tree_url do |tree| + path = tree.path.sub(%r{\A/}, '') + next unless path.present? + + path_segments = path.split('/') + path_segments.pop + parent_tree_path = path_segments.join('/') + + project_tree_path(request.project, File.join(request.ref, parent_tree_path)) + end end diff --git a/app/services/akismet_service.rb b/app/services/akismet_service.rb index 8e11a2a36a7..59153cbbc0a 100644 --- a/app/services/akismet_service.rb +++ b/app/services/akismet_service.rb @@ -58,7 +58,7 @@ class AkismetService } begin - akismet_client.public_send(type, options[:ip_address], options[:user_agent], params) + akismet_client.public_send(type, options[:ip_address], options[:user_agent], params) # rubocop:disable GitlabSecurity/PublicSend true rescue => e Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!") diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb index 6372e5755db..ea3b8d66ed9 100644 --- a/app/services/ci/retry_build_service.rb +++ b/app/services/ci/retry_build_service.rb @@ -23,7 +23,7 @@ module Ci end attributes = CLONE_ACCESSORS.map do |attribute| - [attribute, build.send(attribute)] + [attribute, build.public_send(attribute)] # rubocop:disable GitlabSecurity/PublicSend end attributes.push([:user, current_user]) diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index a48d6a976f0..85c2fcf9ea6 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -11,6 +11,7 @@ module Commits def commit_change(action) raise NotImplementedError unless repository.respond_to?(action) + # rubocop:disable GitlabSecurity/PublicSend repository.public_send( action, current_user, diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index ada2b64a3a6..e81a56672e2 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -90,8 +90,19 @@ class GitPushService < BaseService end def update_signatures - @push_commits.each do |commit| - CreateGpgSignatureWorker.perform_async(commit.sha, @project.id) + commit_shas = @push_commits.last(PROCESS_COMMIT_LIMIT).map(&:sha) + + return if commit_shas.empty? + + shas_with_cached_signatures = GpgSignature.where(commit_sha: commit_shas).pluck(:commit_sha) + commit_shas -= shas_with_cached_signatures + + return if commit_shas.empty? + + commit_shas = Gitlab::Git::Commit.shas_with_signatures(project.repository, commit_shas) + + commit_shas.each do |sha| + CreateGpgSignatureWorker.perform_async(sha, project.id) end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index b84a6fd2b7d..4a4f2b91182 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -338,7 +338,7 @@ class IssuableBaseService < BaseService def invalidate_cache_counts(issuable, users: [], skip_project_cache: false) users.each do |user| - user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") + user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") # rubocop:disable GitlabSecurity/PublicSend end unless skip_project_cache diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index 2e089149ca8..46c505baf8b 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -31,7 +31,7 @@ module Members source.members.find_by(condition) || source.requesters.find_by!(condition) else - source.public_send(scope).find_by!(condition) + source.public_send(scope).find_by!(condition) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index fa0c0b7175c..194413bf321 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -25,7 +25,6 @@ module MergeRequests end def after_create(issuable) - event_service.open_mr(issuable, current_user) todo_service.new_merge_request(issuable, current_user) issuable.cache_merge_request_closes_issues!(current_user) update_merge_requests_head_pipeline(issuable) diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 4267879b03d..e2a80db06a6 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -1,3 +1,5 @@ +# rubocop:disable GitlabSecurity/PublicSend + # NotificationService class # # Used for notifying users with emails about different events diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb index cbcd4478af6..a1c2f8d0180 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -4,7 +4,7 @@ class SystemHooksService end def execute_hooks(data, hooks_scope = :all) - SystemHook.public_send(hooks_scope).find_each do |hook| + SystemHook.public_send(hooks_scope).find_each do |hook| # rubocop:disable GitlabSecurity/PublicSend hook.async_execute(data, 'system_hooks') end end diff --git a/app/services/test_hooks/base_service.rb b/app/services/test_hooks/base_service.rb index 74ba814afff..4abd2c44b2f 100644 --- a/app/services/test_hooks/base_service.rb +++ b/app/services/test_hooks/base_service.rb @@ -18,7 +18,7 @@ module TestHooks end error_message = catch(:validation_error) do - sample_data = self.__send__(trigger_data_method) + sample_data = self.__send__(trigger_data_method) # rubocop:disable GitlabSecurity/PublicSend return hook.execute(sample_data, trigger) end diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index f18c3a74120..445f0dffbcc 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -189,7 +189,7 @@ = icon('chevron-down') %ul.dropdown-menu %li - %a Sort by date + = link_to 'Sort by date', '#' = link_to 'New issue', '#', class: 'btn btn-new btn-inverted' diff --git a/app/views/layouts/nav/_new_admin_sidebar.html.haml b/app/views/layouts/nav/_new_admin_sidebar.html.haml index 0b4a9d92bea..3cbcd841aff 100644 --- a/app/views/layouts/nav/_new_admin_sidebar.html.haml +++ b/app/views/layouts/nav/_new_admin_sidebar.html.haml @@ -1,150 +1,151 @@ .nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } - .context-header - = link_to admin_root_path, title: 'Admin Overview' do - .avatar-container.s40.settings-avatar - = icon('wrench') - .project-title Admin Area - %ul.sidebar-top-level-items - = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: {class: 'home'}) do - = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do - .nav-icon-container - = custom_icon('overview') - %span.nav-item-name - Overview - - %ul.sidebar-sub-level-items - = nav_link(controller: :dashboard, html_options: {class: 'home'}) do - = link_to admin_root_path, title: 'Overview' do - %span - Dashboard - = nav_link(controller: [:admin, :projects]) do - = link_to admin_projects_path, title: 'Projects' do - %span - Projects - = nav_link(controller: :users) do - = link_to admin_users_path, title: 'Users' do - %span - Users - = nav_link(controller: :groups) do - = link_to admin_groups_path, title: 'Groups' do - %span - Groups - = nav_link path: 'jobs#index' do - = link_to admin_jobs_path, title: 'Jobs' do - %span - Jobs - = nav_link path: ['runners#index', 'runners#show'] do - = link_to admin_runners_path, title: 'Runners' do - %span - Runners - = nav_link path: 'cohorts#index' do - = link_to admin_cohorts_path, title: 'Cohorts' do - %span - Cohorts + .nav-sidebar-inner-scroll + .context-header + = link_to admin_root_path, title: 'Admin Overview' do + .avatar-container.s40.settings-avatar + = icon('wrench') + .project-title Admin Area + %ul.sidebar-top-level-items + = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: {class: 'home'}) do + = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do + .nav-icon-container + = custom_icon('overview') + %span.nav-item-name + Overview - = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do - = link_to admin_conversational_development_index_path, title: 'Monitoring' do - .nav-icon-container - = custom_icon('monitoring') - %span.nav-item-name - Monitoring + %ul.sidebar-sub-level-items + = nav_link(controller: :dashboard, html_options: {class: 'home'}) do + = link_to admin_root_path, title: 'Overview' do + %span + Dashboard + = nav_link(controller: [:admin, :projects]) do + = link_to admin_projects_path, title: 'Projects' do + %span + Projects + = nav_link(controller: :users) do + = link_to admin_users_path, title: 'Users' do + %span + Users + = nav_link(controller: :groups) do + = link_to admin_groups_path, title: 'Groups' do + %span + Groups + = nav_link path: 'jobs#index' do + = link_to admin_jobs_path, title: 'Jobs' do + %span + Jobs + = nav_link path: ['runners#index', 'runners#show'] do + = link_to admin_runners_path, title: 'Runners' do + %span + Runners + = nav_link path: 'cohorts#index' do + = link_to admin_cohorts_path, title: 'Cohorts' do + %span + Cohorts - %ul.sidebar-sub-level-items - = nav_link(controller: :conversational_development_index) do - = link_to admin_conversational_development_index_path, title: 'ConvDev Index' do - %span - ConvDev Index - = nav_link(controller: :system_info) do - = link_to admin_system_info_path, title: 'System Info' do - %span - System Info - = nav_link(controller: :background_jobs) do - = link_to admin_background_jobs_path, title: 'Background Jobs' do - %span - Background Jobs - = nav_link(controller: :logs) do - = link_to admin_logs_path, title: 'Logs' do - %span - Logs - = nav_link(controller: :health_check) do - = link_to admin_health_check_path, title: 'Health Check' do - %span - Health Check - = nav_link(controller: :requests_profiles) do - = link_to admin_requests_profiles_path, title: 'Requests Profiles' do - %span - Requests Profiles + = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do + = link_to admin_conversational_development_index_path, title: 'Monitoring' do + .nav-icon-container + = custom_icon('monitoring') + %span.nav-item-name + Monitoring - = nav_link(controller: :broadcast_messages) do - = link_to admin_broadcast_messages_path, title: 'Messages' do - .nav-icon-container - = custom_icon('messages') - %span.nav-item-name - Messages - = nav_link(controller: [:hooks, :hook_logs]) do - = link_to admin_hooks_path, title: 'Hooks' do - .nav-icon-container - = custom_icon('system_hooks') - %span.nav-item-name - System Hooks + %ul.sidebar-sub-level-items + = nav_link(controller: :conversational_development_index) do + = link_to admin_conversational_development_index_path, title: 'ConvDev Index' do + %span + ConvDev Index + = nav_link(controller: :system_info) do + = link_to admin_system_info_path, title: 'System Info' do + %span + System Info + = nav_link(controller: :background_jobs) do + = link_to admin_background_jobs_path, title: 'Background Jobs' do + %span + Background Jobs + = nav_link(controller: :logs) do + = link_to admin_logs_path, title: 'Logs' do + %span + Logs + = nav_link(controller: :health_check) do + = link_to admin_health_check_path, title: 'Health Check' do + %span + Health Check + = nav_link(controller: :requests_profiles) do + = link_to admin_requests_profiles_path, title: 'Requests Profiles' do + %span + Requests Profiles - = nav_link(controller: :applications) do - = link_to admin_applications_path, title: 'Applications' do - .nav-icon-container - = custom_icon('applications') - %span.nav-item-name - Applications + = nav_link(controller: :broadcast_messages) do + = link_to admin_broadcast_messages_path, title: 'Messages' do + .nav-icon-container + = custom_icon('messages') + %span.nav-item-name + Messages + = nav_link(controller: [:hooks, :hook_logs]) do + = link_to admin_hooks_path, title: 'Hooks' do + .nav-icon-container + = custom_icon('system_hooks') + %span.nav-item-name + System Hooks - = nav_link(controller: :abuse_reports) do - = link_to admin_abuse_reports_path, title: "Abuse Reports" do - .nav-icon-container - = custom_icon('abuse_reports') - %span.nav-item-name - Abuse Reports - %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) + = nav_link(controller: :applications) do + = link_to admin_applications_path, title: 'Applications' do + .nav-icon-container + = custom_icon('applications') + %span.nav-item-name + Applications - - if akismet_enabled? - = nav_link(controller: :spam_logs) do - = link_to admin_spam_logs_path, title: "Spam Logs" do + = nav_link(controller: :abuse_reports) do + = link_to admin_abuse_reports_path, title: "Abuse Reports" do .nav-icon-container - = custom_icon('spam_logs') + = custom_icon('abuse_reports') %span.nav-item-name - Spam Logs + Abuse Reports + %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) - = nav_link(controller: :deploy_keys) do - = link_to admin_deploy_keys_path, title: 'Deploy Keys' do - .nav-icon-container - = custom_icon('key') - %span.nav-item-name - Deploy Keys + - if akismet_enabled? + = nav_link(controller: :spam_logs) do + = link_to admin_spam_logs_path, title: "Spam Logs" do + .nav-icon-container + = custom_icon('spam_logs') + %span.nav-item-name + Spam Logs - = nav_link(controller: :services) do - = link_to admin_application_settings_services_path, title: 'Service Templates' do - .nav-icon-container - = custom_icon('service_templates') - %span.nav-item-name - Service Templates + = nav_link(controller: :deploy_keys) do + = link_to admin_deploy_keys_path, title: 'Deploy Keys' do + .nav-icon-container + = custom_icon('key') + %span.nav-item-name + Deploy Keys - = nav_link(controller: :labels) do - = link_to admin_labels_path, title: 'Labels' do - .nav-icon-container - = custom_icon('labels') - %span.nav-item-name - Labels + = nav_link(controller: :services) do + = link_to admin_application_settings_services_path, title: 'Service Templates' do + .nav-icon-container + = custom_icon('service_templates') + %span.nav-item-name + Service Templates - = nav_link(controller: :appearances) do - = link_to admin_appearances_path, title: 'Appearances' do - .nav-icon-container - = custom_icon('appearance') - %span.nav-item-name - Appearance + = nav_link(controller: :labels) do + = link_to admin_labels_path, title: 'Labels' do + .nav-icon-container + = custom_icon('labels') + %span.nav-item-name + Labels - %li.divider - = nav_link(controller: :application_settings) do - = link_to admin_application_settings_path, title: 'Settings' do - .nav-icon-container - = custom_icon('settings') - %span.nav-item-name - Settings + = nav_link(controller: :appearances) do + = link_to admin_appearances_path, title: 'Appearances' do + .nav-icon-container + = custom_icon('appearance') + %span.nav-item-name + Appearance + + %li.divider + = nav_link(controller: :application_settings) do + = link_to admin_application_settings_path, title: 'Settings' do + .nav-icon-container + = custom_icon('settings') + %span.nav-item-name + Settings - = render 'shared/sidebar_toggle_button' + = render 'shared/sidebar_toggle_button' diff --git a/app/views/layouts/nav/_new_group_sidebar.html.haml b/app/views/layouts/nav/_new_group_sidebar.html.haml index c7dabbd8237..ed5793f09fe 100644 --- a/app/views/layouts/nav/_new_group_sidebar.html.haml +++ b/app/views/layouts/nav/_new_group_sidebar.html.haml @@ -1,89 +1,90 @@ .nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } - .context-header - = link_to group_path(@group), title: @group.name do - .avatar-container.s40.group-avatar - = image_tag group_icon(@group), class: "avatar s40 avatar-tile" - .group-title - = @group.name - %ul.sidebar-top-level-items - = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do - = link_to group_path(@group), title: 'Group overview' do - .nav-icon-container - = custom_icon('project') - %span.nav-item-name - Overview - - %ul.sidebar-sub-level-items - = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do - = link_to group_path(@group), title: 'Group details' do - %span - Details - - = nav_link(path: 'groups#activity') do - = link_to activity_group_path(@group), title: 'Activity' do - %span - Activity - - = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do - = link_to issues_group_path(@group), title: 'Issues' do - .nav-icon-container - = custom_icon('issues') - %span.nav-item-name - - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - Issues - %span.badge.count= number_with_delimiter(issues.count) - - %ul.sidebar-sub-level-items - = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do - = link_to issues_group_path(@group), title: 'List' do - %span - List + .nav-sidebar-inner-scroll + .context-header + = link_to group_path(@group), title: @group.name do + .avatar-container.s40.group-avatar + = image_tag group_icon(@group), class: "avatar s40 avatar-tile" + .group-title + = @group.name + %ul.sidebar-top-level-items + = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do + = link_to group_path(@group), title: 'Group overview' do + .nav-icon-container + = custom_icon('project') + %span.nav-item-name + Overview - = nav_link(path: 'labels#index') do - = link_to group_labels_path(@group), title: 'Labels' do - %span - Labels + %ul.sidebar-sub-level-items + = nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do + = link_to group_path(@group), title: 'Group details' do + %span + Details - = nav_link(path: 'milestones#index') do - = link_to group_milestones_path(@group), title: 'Milestones' do - %span - Milestones + = nav_link(path: 'groups#activity') do + = link_to activity_group_path(@group), title: 'Activity' do + %span + Activity - = nav_link(path: 'groups#merge_requests') do - = link_to merge_requests_group_path(@group), title: 'Merge Requests' do - .nav-icon-container - = custom_icon('mr_bold') - %span.nav-item-name - - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute - Merge Requests - %span.badge.count= number_with_delimiter(merge_requests.count) - = nav_link(path: 'group_members#index') do - = link_to group_group_members_path(@group), title: 'Members' do - .nav-icon-container - = custom_icon('members') - %span.nav-item-name - Members - - if current_user && can?(current_user, :admin_group, @group) - = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do - = link_to edit_group_path(@group), title: 'Settings' do + = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do + = link_to issues_group_path(@group), title: 'Issues' do .nav-icon-container - = custom_icon('settings') + = custom_icon('issues') %span.nav-item-name - Settings + - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute + Issues + %span.badge.count= number_with_delimiter(issues.count) + %ul.sidebar-sub-level-items - = nav_link(path: 'groups#edit') do - = link_to edit_group_path(@group), title: 'General' do + = nav_link(path: 'groups#issues', html_options: { class: 'home' }) do + = link_to issues_group_path(@group), title: 'List' do %span - General + List - = nav_link(path: 'groups#projects') do - = link_to projects_group_path(@group), title: 'Projects' do + = nav_link(path: 'labels#index') do + = link_to group_labels_path(@group), title: 'Labels' do %span - Projects + Labels - = nav_link(controller: :ci_cd) do - = link_to group_settings_ci_cd_path(@group), title: 'CI / CD' do + = nav_link(path: 'milestones#index') do + = link_to group_milestones_path(@group), title: 'Milestones' do %span - CI / CD + Milestones + + = nav_link(path: 'groups#merge_requests') do + = link_to merge_requests_group_path(@group), title: 'Merge Requests' do + .nav-icon-container + = custom_icon('mr_bold') + %span.nav-item-name + - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute + Merge Requests + %span.badge.count= number_with_delimiter(merge_requests.count) + = nav_link(path: 'group_members#index') do + = link_to group_group_members_path(@group), title: 'Members' do + .nav-icon-container + = custom_icon('members') + %span.nav-item-name + Members + - if current_user && can?(current_user, :admin_group, @group) + = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do + = link_to edit_group_path(@group), title: 'Settings' do + .nav-icon-container + = custom_icon('settings') + %span.nav-item-name + Settings + %ul.sidebar-sub-level-items + = nav_link(path: 'groups#edit') do + = link_to edit_group_path(@group), title: 'General' do + %span + General + + = nav_link(path: 'groups#projects') do + = link_to projects_group_path(@group), title: 'Projects' do + %span + Projects + + = nav_link(controller: :ci_cd) do + = link_to group_settings_ci_cd_path(@group), title: 'CI / CD' do + %span + CI / CD - = render 'shared/sidebar_toggle_button' + = render 'shared/sidebar_toggle_button' diff --git a/app/views/layouts/nav/_new_profile_sidebar.html.haml b/app/views/layouts/nav/_new_profile_sidebar.html.haml index edae009a28e..4234df56d1d 100644 --- a/app/views/layouts/nav/_new_profile_sidebar.html.haml +++ b/app/views/layouts/nav/_new_profile_sidebar.html.haml @@ -1,84 +1,85 @@ .nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } - .context-header - = link_to profile_path, title: 'Profile Settings' do - .avatar-container.s40.settings-avatar - = icon('user') - .project-title User Settings - %ul.sidebar-top-level-items - = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do + .nav-sidebar-inner-scroll + .context-header = link_to profile_path, title: 'Profile Settings' do - .nav-icon-container - = custom_icon('profile') - %span.nav-item-name - Profile - = nav_link(controller: [:accounts, :two_factor_auths]) do - = link_to profile_account_path, title: 'Account' do - .nav-icon-container - = custom_icon('account') - %span.nav-item-name - Account - - if current_application_settings.user_oauth_applications? - = nav_link(controller: 'oauth/applications') do - = link_to applications_profile_path, title: 'Applications' do + .avatar-container.s40.settings-avatar + = icon('user') + .project-title User Settings + %ul.sidebar-top-level-items + = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do + = link_to profile_path, title: 'Profile Settings' do .nav-icon-container - = custom_icon('applications') + = custom_icon('profile') %span.nav-item-name - Applications - = nav_link(controller: :chat_names) do - = link_to profile_chat_names_path, title: 'Chat' do - .nav-icon-container - = custom_icon('chat') - %span.nav-item-name - Chat - = nav_link(controller: :personal_access_tokens) do - = link_to profile_personal_access_tokens_path, title: 'Access Tokens' do - .nav-icon-container - = custom_icon('access_tokens') - %span.nav-item-name - Access Tokens - = nav_link(controller: :emails) do - = link_to profile_emails_path, title: 'Emails' do - .nav-icon-container - = custom_icon('emails') - %span.nav-item-name - Emails - - unless current_user.ldap_user? - = nav_link(controller: :passwords) do - = link_to edit_profile_password_path, title: 'Password' do + Profile + = nav_link(controller: [:accounts, :two_factor_auths]) do + = link_to profile_account_path, title: 'Account' do .nav-icon-container - = custom_icon('lock') + = custom_icon('account') %span.nav-item-name - Password - = nav_link(controller: :notifications) do - = link_to profile_notifications_path, title: 'Notifications' do - .nav-icon-container - = custom_icon('notifications') - %span.nav-item-name - Notifications + Account + - if current_application_settings.user_oauth_applications? + = nav_link(controller: 'oauth/applications') do + = link_to applications_profile_path, title: 'Applications' do + .nav-icon-container + = custom_icon('applications') + %span.nav-item-name + Applications + = nav_link(controller: :chat_names) do + = link_to profile_chat_names_path, title: 'Chat' do + .nav-icon-container + = custom_icon('chat') + %span.nav-item-name + Chat + = nav_link(controller: :personal_access_tokens) do + = link_to profile_personal_access_tokens_path, title: 'Access Tokens' do + .nav-icon-container + = custom_icon('access_tokens') + %span.nav-item-name + Access Tokens + = nav_link(controller: :emails) do + = link_to profile_emails_path, title: 'Emails' do + .nav-icon-container + = custom_icon('emails') + %span.nav-item-name + Emails + - unless current_user.ldap_user? + = nav_link(controller: :passwords) do + = link_to edit_profile_password_path, title: 'Password' do + .nav-icon-container + = custom_icon('lock') + %span.nav-item-name + Password + = nav_link(controller: :notifications) do + = link_to profile_notifications_path, title: 'Notifications' do + .nav-icon-container + = custom_icon('notifications') + %span.nav-item-name + Notifications - = nav_link(controller: :keys) do - = link_to profile_keys_path, title: 'SSH Keys' do - .nav-icon-container - = custom_icon('key') - %span.nav-item-name - SSH Keys - = nav_link(controller: :gpg_keys) do - = link_to profile_gpg_keys_path, title: 'GPG Keys' do - .nav-icon-container - = custom_icon('key_2') - %span.nav-item-name - GPG Keys - = nav_link(controller: :preferences) do - = link_to profile_preferences_path, title: 'Preferences' do - .nav-icon-container - = custom_icon('preferences') - %span.nav-item-name - Preferences - = nav_link(path: 'profiles#audit_log') do - = link_to audit_log_profile_path, title: 'Authentication log' do - .nav-icon-container - = custom_icon('authentication_log') - %span.nav-item-name - Authentication log + = nav_link(controller: :keys) do + = link_to profile_keys_path, title: 'SSH Keys' do + .nav-icon-container + = custom_icon('key') + %span.nav-item-name + SSH Keys + = nav_link(controller: :gpg_keys) do + = link_to profile_gpg_keys_path, title: 'GPG Keys' do + .nav-icon-container + = custom_icon('key_2') + %span.nav-item-name + GPG Keys + = nav_link(controller: :preferences) do + = link_to profile_preferences_path, title: 'Preferences' do + .nav-icon-container + = custom_icon('preferences') + %span.nav-item-name + Preferences + = nav_link(path: 'profiles#audit_log') do + = link_to audit_log_profile_path, title: 'Authentication log' do + .nav-icon-container + = custom_icon('authentication_log') + %span.nav-item-name + Authentication log - = render 'shared/sidebar_toggle_button' + = render 'shared/sidebar_toggle_button' diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/_new_project_sidebar.html.haml index e0477c29ebe..0ef81375c3a 100644 --- a/app/views/layouts/nav/_new_project_sidebar.html.haml +++ b/app/views/layouts/nav/_new_project_sidebar.html.haml @@ -1,261 +1,262 @@ .nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } - - can_edit = can?(current_user, :admin_project, @project) - .context-header - = link_to project_path(@project), title: @project.name do - .avatar-container.s40.project-avatar - = project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile') - .project-title - = @project.name - %ul.sidebar-top-level-items - = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do - = link_to project_path(@project), title: 'Project overview', class: 'shortcuts-project' do - .nav-icon-container - = custom_icon('project') - %span.nav-item-name - Overview - - %ul.sidebar-sub-level-items - = nav_link(path: 'projects#show') do - = link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do - %span= _('Details') - - = nav_link(path: 'projects#activity') do - = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do - %span= _('Activity') - - - if can?(current_user, :read_cycle_analytics, @project) - = nav_link(path: 'cycle_analytics#show') do - = link_to project_cycle_analytics_path(@project), title: _('Cycle Analytics'), class: 'shortcuts-project-cycle-analytics' do - %span= _('Cycle Analytics') - - - if project_nav_tab? :files - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do - = link_to project_tree_path(@project), title: 'Repository', class: 'shortcuts-tree' do + .nav-sidebar-inner-scroll + - can_edit = can?(current_user, :admin_project, @project) + .context-header + = link_to project_path(@project), title: @project.name do + .avatar-container.s40.project-avatar + = project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile') + .project-title + = @project.name + %ul.sidebar-top-level-items + = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do + = link_to project_path(@project), title: 'Project overview', class: 'shortcuts-project' do .nav-icon-container - = custom_icon('doc_text') + = custom_icon('project') %span.nav-item-name - Repository + Overview %ul.sidebar-sub-level-items - = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do - = link_to project_tree_path(@project) do - #{ _('Files') } - - = nav_link(controller: [:commit, :commits]) do - = link_to project_commits_path(@project, current_ref) do - #{ _('Commits') } - - = nav_link(html_options: {class: branches_tab_class}) do - = link_to project_branches_path(@project) do - #{ _('Branches') } - - = nav_link(controller: [:tags, :releases]) do - = link_to project_tags_path(@project) do - #{ _('Tags') } - - = nav_link(path: 'graphs#show') do - = link_to project_graph_path(@project, current_ref) do - #{ _('Contributors') } - - = nav_link(controller: %w(network)) do - = link_to project_network_path(@project, current_ref) do - #{ s_('ProjectNetworkGraph|Graph') } - - = nav_link(controller: :compare) do - = link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do - #{ _('Compare') } - - = nav_link(path: 'graphs#charts') do - = link_to charts_project_graph_path(@project, current_ref) do - #{ _('Charts') } - - - if project_nav_tab? :container_registry - = nav_link(controller: %w[projects/registry/repositories]) do - = link_to project_container_registry_index_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do - .nav-icon-container - = custom_icon('container_registry') - %span.nav-item-name - Registry - - - if project_nav_tab? :issues - = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do - = link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do - .nav-icon-container - = custom_icon('issues') - %span.nav-item-name - Issues - - if @project.issues_enabled? - %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) - - %ul.sidebar-sub-level-items - = nav_link(controller: :issues) do - = link_to project_issues_path(@project), title: 'Issues' do - %span - List - - = nav_link(controller: :boards) do - = link_to project_boards_path(@project), title: 'Board' do - %span - Board - - = nav_link(controller: :labels) do - = link_to project_labels_path(@project), title: 'Labels' do - %span - Labels - - = nav_link(controller: :milestones) do - = link_to project_milestones_path(@project), title: 'Milestones' do - %span - Milestones - - - if project_nav_tab? :merge_requests - = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do - = link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do - .nav-icon-container - = custom_icon('mr_bold') - %span.nav-item-name - Merge Requests - %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) - - - if project_nav_tab? :pipelines - = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do - = link_to project_pipelines_path(@project), title: 'CI / CD', class: 'shortcuts-pipelines' do - .nav-icon-container - = custom_icon('pipeline') - %span.nav-item-name - CI / CD - - %ul.sidebar-sub-level-items - - if project_nav_tab? :pipelines - = nav_link(path: ['pipelines#index', 'pipelines#show']) do - = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do - %span - Pipelines - - - if project_nav_tab? :builds - = nav_link(controller: [:jobs, :artifacts]) do - = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do + = nav_link(path: 'projects#show') do + = link_to project_path(@project), title: _('Project details'), class: 'shortcuts-project' do + %span= _('Details') + + = nav_link(path: 'projects#activity') do + = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do + %span= _('Activity') + + - if can?(current_user, :read_cycle_analytics, @project) + = nav_link(path: 'cycle_analytics#show') do + = link_to project_cycle_analytics_path(@project), title: _('Cycle Analytics'), class: 'shortcuts-project-cycle-analytics' do + %span= _('Cycle Analytics') + + - if project_nav_tab? :files + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do + = link_to project_tree_path(@project), title: 'Repository', class: 'shortcuts-tree' do + .nav-icon-container + = custom_icon('doc_text') + %span.nav-item-name + Repository + + %ul.sidebar-sub-level-items + = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do + = link_to project_tree_path(@project) do + #{ _('Files') } + + = nav_link(controller: [:commit, :commits]) do + = link_to project_commits_path(@project, current_ref) do + #{ _('Commits') } + + = nav_link(html_options: {class: branches_tab_class}) do + = link_to project_branches_path(@project) do + #{ _('Branches') } + + = nav_link(controller: [:tags, :releases]) do + = link_to project_tags_path(@project) do + #{ _('Tags') } + + = nav_link(path: 'graphs#show') do + = link_to project_graph_path(@project, current_ref) do + #{ _('Contributors') } + + = nav_link(controller: %w(network)) do + = link_to project_network_path(@project, current_ref) do + #{ s_('ProjectNetworkGraph|Graph') } + + = nav_link(controller: :compare) do + = link_to project_compare_index_path(@project, from: @repository.root_ref, to: current_ref) do + #{ _('Compare') } + + = nav_link(path: 'graphs#charts') do + = link_to charts_project_graph_path(@project, current_ref) do + #{ _('Charts') } + + - if project_nav_tab? :container_registry + = nav_link(controller: %w[projects/registry/repositories]) do + = link_to project_container_registry_index_path(@project), title: 'Container Registry', class: 'shortcuts-container-registry' do + .nav-icon-container + = custom_icon('container_registry') + %span.nav-item-name + Registry + + - if project_nav_tab? :issues + = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do + = link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do + .nav-icon-container + = custom_icon('issues') + %span.nav-item-name + Issues + - if @project.issues_enabled? + %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) + + %ul.sidebar-sub-level-items + = nav_link(controller: :issues) do + = link_to project_issues_path(@project), title: 'Issues' do %span - Jobs + List - - if project_nav_tab? :pipelines - = nav_link(controller: :pipeline_schedules) do - = link_to pipeline_schedules_path(@project), title: 'Schedules', class: 'shortcuts-builds' do + = nav_link(controller: :boards) do + = link_to project_boards_path(@project), title: 'Board' do %span - Schedules + Board - - if project_nav_tab? :environments - = nav_link(controller: :environments) do - = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do + = nav_link(controller: :labels) do + = link_to project_labels_path(@project), title: 'Labels' do %span - Environments + Labels - - if @project.feature_available?(:builds, current_user) && !@project.empty_repo? - = nav_link(path: 'pipelines#charts') do - = link_to charts_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do + = nav_link(controller: :milestones) do + = link_to project_milestones_path(@project), title: 'Milestones' do %span - Charts + Milestones + + - if project_nav_tab? :merge_requests + = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do + = link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do + .nav-icon-container + = custom_icon('mr_bold') + %span.nav-item-name + Merge Requests + %span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) + + - if project_nav_tab? :pipelines + = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do + = link_to project_pipelines_path(@project), title: 'CI / CD', class: 'shortcuts-pipelines' do + .nav-icon-container + = custom_icon('pipeline') + %span.nav-item-name + CI / CD + + %ul.sidebar-sub-level-items + - if project_nav_tab? :pipelines + = nav_link(path: ['pipelines#index', 'pipelines#show']) do + = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do + %span + Pipelines - - if project_nav_tab? :wiki - = nav_link(controller: :wikis) do - = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do - .nav-icon-container - = custom_icon('wiki') - %span.nav-item-name - Wiki + - if project_nav_tab? :builds + = nav_link(controller: [:jobs, :artifacts]) do + = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do + %span + Jobs - - if project_nav_tab? :snippets - = nav_link(controller: :snippets) do - = link_to project_snippets_path(@project), title: 'Snippets', class: 'shortcuts-snippets' do - .nav-icon-container - = custom_icon('snippets') - %span.nav-item-name - Snippets + - if project_nav_tab? :pipelines + = nav_link(controller: :pipeline_schedules) do + = link_to pipeline_schedules_path(@project), title: 'Schedules', class: 'shortcuts-builds' do + %span + Schedules - - if project_nav_tab? :settings - = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do - = link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do - .nav-icon-container - = custom_icon('settings') - %span.nav-item-name - Settings + - if project_nav_tab? :environments + = nav_link(controller: :environments) do + = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do + %span + Environments - %ul.sidebar-sub-level-items - - can_edit = can?(current_user, :admin_project, @project) - - if can_edit - = nav_link(path: %w[projects#edit]) do - = link_to edit_project_path(@project), title: 'General' do - %span - General - = nav_link(controller: :project_members) do - = link_to project_project_members_path(@project), title: 'Members' do - %span - Members - - if can_edit - = nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do - = link_to project_settings_integrations_path(@project), title: 'Integrations' do - %span - Integrations - = nav_link(controller: :repository) do - = link_to project_settings_repository_path(@project), title: 'Repository' do + - if @project.feature_available?(:builds, current_user) && !@project.empty_repo? + = nav_link(path: 'pipelines#charts') do + = link_to charts_project_pipelines_path(@project), title: 'Charts', class: 'shortcuts-pipelines-charts' do + %span + Charts + + - if project_nav_tab? :wiki + = nav_link(controller: :wikis) do + = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do + .nav-icon-container + = custom_icon('wiki') + %span.nav-item-name + Wiki + + - if project_nav_tab? :snippets + = nav_link(controller: :snippets) do + = link_to project_snippets_path(@project), title: 'Snippets', class: 'shortcuts-snippets' do + .nav-icon-container + = custom_icon('snippets') + %span.nav-item-name + Snippets + + - if project_nav_tab? :settings + = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do + = link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do + .nav-icon-container + = custom_icon('settings') + %span.nav-item-name + Settings + + %ul.sidebar-sub-level-items + - can_edit = can?(current_user, :admin_project, @project) + - if can_edit + = nav_link(path: %w[projects#edit]) do + = link_to edit_project_path(@project), title: 'General' do + %span + General + = nav_link(controller: :project_members) do + = link_to project_project_members_path(@project), title: 'Members' do %span - Repository - - if @project.feature_available?(:builds, current_user) - = nav_link(controller: :ci_cd) do - = link_to project_settings_ci_cd_path(@project), title: 'CI / CD' do + Members + - if can_edit + = nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do + = link_to project_settings_integrations_path(@project), title: 'Integrations' do %span - CI / CD - - if Gitlab.config.pages.enabled - = nav_link(controller: :pages) do - = link_to project_pages_path(@project), title: 'Pages' do + Integrations + = nav_link(controller: :repository) do + = link_to project_settings_repository_path(@project), title: 'Repository' do %span - Pages - - - else - = nav_link(path: %w[members#show]) do - = link_to project_settings_members_path(@project), title: 'Members', class: 'shortcuts-tree' do - .nav-icon-container - = custom_icon('members') - %span.nav-item-name - Members - - = render 'shared/sidebar_toggle_button' - - -# Shortcut to Project > Activity - %li.hidden - = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do - %span - Activity - - -# Shortcut to Repository > Graph (formerly, Network) - - if project_nav_tab? :network + Repository + - if @project.feature_available?(:builds, current_user) + = nav_link(controller: :ci_cd) do + = link_to project_settings_ci_cd_path(@project), title: 'CI / CD' do + %span + CI / CD + - if Gitlab.config.pages.enabled + = nav_link(controller: :pages) do + = link_to project_pages_path(@project), title: 'Pages' do + %span + Pages + + - else + = nav_link(path: %w[members#show]) do + = link_to project_settings_members_path(@project), title: 'Members', class: 'shortcuts-tree' do + .nav-icon-container + = custom_icon('members') + %span.nav-item-name + Members + + = render 'shared/sidebar_toggle_button' + + -# Shortcut to Project > Activity %li.hidden - = link_to project_network_path(@project, current_ref), title: 'Network', class: 'shortcuts-network' do - Graph - - -# Shortcut to Repository > Charts (formerly, top-nav item "Graphs") - - unless @project.empty_repo? - %li.hidden - = link_to charts_project_graph_path(@project, current_ref), title: 'Charts', class: 'shortcuts-repository-charts' do - Charts - - -# Shortcut to Issues > New Issue - %li.hidden - = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do - Create a new issue - - -# Shortcut to Pipelines > Jobs - - if project_nav_tab? :builds + = link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do + %span + Activity + + -# Shortcut to Repository > Graph (formerly, Network) + - if project_nav_tab? :network + %li.hidden + = link_to project_network_path(@project, current_ref), title: 'Network', class: 'shortcuts-network' do + Graph + + -# Shortcut to Repository > Charts (formerly, top-nav item "Graphs") + - unless @project.empty_repo? + %li.hidden + = link_to charts_project_graph_path(@project, current_ref), title: 'Charts', class: 'shortcuts-repository-charts' do + Charts + + -# Shortcut to Issues > New Issue %li.hidden - = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do - Jobs - - -# Shortcut to commits page - - if project_nav_tab? :commits + = link_to new_project_issue_path(@project), class: 'shortcuts-new-issue' do + Create a new issue + + -# Shortcut to Pipelines > Jobs + - if project_nav_tab? :builds + %li.hidden + = link_to project_jobs_path(@project), title: 'Jobs', class: 'shortcuts-builds' do + Jobs + + -# Shortcut to commits page + - if project_nav_tab? :commits + %li.hidden + = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do + Commits + + -# Shortcut to issue boards %li.hidden - = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do - Commits - - -# Shortcut to issue boards - %li.hidden - = link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards' + = link_to 'Issue Boards', project_boards_path(@project), title: 'Issue Boards', class: 'shortcuts-issue-boards' diff --git a/app/views/projects/_project_templates.html.haml b/app/views/projects/_project_templates.html.haml index 21baf35f2ac..97cf13df070 100644 --- a/app/views/projects/_project_templates.html.haml +++ b/app/views/projects/_project_templates.html.haml @@ -5,6 +5,6 @@ Blank - Gitlab::ProjectTemplate.all.each do |template| .btn - %input{ type: "radio", autocomplete: "off", name: "project_templates", id: template.name } + %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name } = custom_icon(template.logo) = template.title diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 178ab3df2e5..376f672f424 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -6,7 +6,7 @@ .content-block.oneline-block.files-changed.diff-files-changed.js-diff-files-changed{ class: ("diff-files-changed-merge-request" if merge_request) } .files-changed-inner - .inline-parallel-buttons + .inline-parallel-buttons.hidden-xs.hidden-sm - if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? } = link_to 'Expand all', url_for(params.merge(expanded: 1, format: nil)), class: 'btn btn-default' - if show_whitespace_toggle diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index efc0ea31917..02fd54c97fb 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -10,7 +10,7 @@ %strong.cgreen #{sum_added_lines} additions and %strong.cred #{sum_removed_lines} deletions - .diff-stats-additions-deletions-collapsed.pull-right{ "aria-hidden": "true", "aria-describedby": "diff-stats" } + .diff-stats-additions-deletions-collapsed.pull-right.hidden-xs.hidden-sm{ "aria-hidden": "true", "aria-describedby": "diff-stats" } %strong.cgreen< +#{sum_added_lines} %strong.cred< diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index b787edb3427..3303aa72604 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -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"} - if @merge_request.reopenable? = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-reopen reopen-mr-link js-note-target-close js-note-target-reopen", title: "Reopen merge request", data: { original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"} - %comment-and-resolve-btn{ "inline-template" => true, ":discussion-id" => "" } - %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 }} + %comment-and-resolve-btn{ "inline-template" => true } + %button.btn.btn-nr.btn-default.append-right-10.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { project_path: "#{project_path(@merge_request.project)}" } } + {{ buttonText }} #notes= render "shared/notes/notes_with_form", :autocomplete => true diff --git a/app/views/projects/merge_requests/_nav_btns.html.haml b/app/views/projects/merge_requests/_nav_btns.html.haml index 436d1aa9e57..e73dab8ad4a 100644 --- a/app/views/projects/merge_requests/_nav_btns.html.haml +++ b/app/views/projects/merge_requests/_nav_btns.html.haml @@ -1,5 +1,5 @@ - if @can_bulk_update - = button_tag "Edit merge requests", class: "btn js-bulk-update-toggle" + = button_tag "Edit merge requests", class: "btn append-right-10 js-bulk-update-toggle" - if merge_project = link_to new_merge_request_path, class: "btn btn-new", title: "New merge request" do New merge request diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index e3bbebbcf4c..647e0a772b1 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -25,7 +25,7 @@ .form-group = f.label :template_project, class: 'label-light' do Create from template - = link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: "What’s included in a template?" }, title: "What’s included in a template?", class: 'has-tooltip', data: { placement: 'top'} + = link_to icon('question-circle'), help_page_path("gitlab-basics/create-project"), target: '_blank', aria: { label: "What’s included in a template?" }, title: "What’s included in a template?", class: 'has-tooltip', data: { placement: 'top'} %div = render 'project_templates', f: f .second-column diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index 4498c8f8349..7ad743b3b81 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -6,7 +6,7 @@ - @options && @options.each do |key, value| = hidden_field_tag key, value, id: nil .dropdown - = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project), field_name: 'ref', submit_form_on_click: true }, { toggle_class: "js-project-refs-dropdown" } + = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project), field_name: 'ref', submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown" } .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) } = dropdown_title _("Switch branch/tag") = dropdown_filter _("Search branches and tags") diff --git a/app/views/shared/_target_switcher.html.haml b/app/views/shared/_target_switcher.html.haml index 3672b552f10..9236868652f 100644 --- a/app/views/shared/_target_switcher.html.haml +++ b/app/views/shared/_target_switcher.html.haml @@ -1,5 +1,5 @@ - dropdown_toggle_text = @ref || @project.default_branch -= form_tag nil, method: :get, class: "project-refs-target-form" do += form_tag nil, method: :get, style: { display: 'none' }, class: "project-refs-target-form" do = hidden_field_tag :destination, destination - if defined?(path) = hidden_field_tag :path, path diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index 6f6a036b13f..6a85f7d0564 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -32,7 +32,7 @@ .col-sm-6.milestone-actions - if can?(current_user, :admin_milestones, @group) - if milestone.is_group_milestone? - = link_to edit_group_milestone_path(@group, milestone.id), class: "btn btn-xs btn-grouped" do + = link_to edit_group_milestone_path(@group, milestone), class: "btn btn-xs btn-grouped" do Edit \ - if milestone.closed? diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml index 66ac8196f2f..40379f48393 100644 --- a/app/views/shared/milestones/_sidebar.html.haml +++ b/app/views/shared/milestones/_sidebar.html.haml @@ -1,7 +1,7 @@ - affix_offset = local_assigns.fetch(:affix_offset, "50") - project = local_assigns[:project] -%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } +%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } .issuable-sidebar.milestone-sidebar .block.milestone-progress.issuable-sidebar-header %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" } diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml index b93837e3087..3014300fbe7 100644 --- a/app/views/shared/milestones/_top.html.haml +++ b/app/views/shared/milestones/_top.html.haml @@ -23,7 +23,7 @@ .pull-right - if can?(current_user, :admin_milestones, group) - if milestone.is_group_milestone? - = link_to edit_group_milestone_path(group, milestone.iid), class: "btn btn btn-grouped" do + = link_to edit_group_milestone_path(group, milestone), class: "btn btn btn-grouped" do Edit - if milestone.active? = link_to 'Close Milestone', group_milestone_route(milestone, {state_event: :close }), method: :put, class: "btn btn-grouped btn-close" diff --git a/app/views/shared/repo/_repo.html.haml b/app/views/shared/repo/_repo.html.haml index 0fc40cf0801..87fa2007d16 100644 --- a/app/views/shared/repo/_repo.html.haml +++ b/app/views/shared/repo/_repo.html.haml @@ -1,2 +1,7 @@ -#repo{ data: { url: content_url, project_name: project.name, refs_url: refs_project_path(project, format: :json), project_url: project_path(project), project_id: project.id, can_commit: (!!can_push_branch?(project, @ref)).to_s } } - %repo +#repo{ data: { url: content_url, + project_name: project.name, + refs_url: refs_project_path(project, format: :json), + project_url: project_path(project), + project_id: project.id, + can_commit: (!!can_push_branch?(project, @ref)).to_s, + on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s } } diff --git a/app/workers/create_gpg_signature_worker.rb b/app/workers/create_gpg_signature_worker.rb index 4f47717ff69..f34dff2d656 100644 --- a/app/workers/create_gpg_signature_worker.rb +++ b/app/workers/create_gpg_signature_worker.rb @@ -4,13 +4,9 @@ class CreateGpgSignatureWorker def perform(commit_sha, project_id) project = Project.find_by(id: project_id) - return unless project - commit = project.commit(commit_sha) - - return unless commit - - commit.signature + # This calculates and caches the signature in the database + Gitlab::Gpg::Commit.new(project, commit_sha).signature end end diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb index 964287a1793..0ec871e00e1 100644 --- a/app/workers/gitlab_shell_worker.rb +++ b/app/workers/gitlab_shell_worker.rb @@ -4,6 +4,6 @@ class GitlabShellWorker include DedicatedSidekiqQueue def perform(action, *arg) - gitlab_shell.send(action, *arg) + gitlab_shell.__send__(action, *arg) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/workers/namespaceless_project_destroy_worker.rb b/app/workers/namespaceless_project_destroy_worker.rb index bfae0c77700..a9073742ff7 100644 --- a/app/workers/namespaceless_project_destroy_worker.rb +++ b/app/workers/namespaceless_project_destroy_worker.rb @@ -24,10 +24,6 @@ class NamespacelessProjectDestroyWorker unlink_fork(project) if project.forked? - # Override Project#remove_pages for this instance so it doesn't do anything - def project.remove_pages - end - project.destroy! end diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index a338523dc6b..cde5b45ad41 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -5,14 +5,17 @@ class RepositoryForkWorker include Gitlab::ShellAdapter include DedicatedSidekiqQueue + sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION + def perform(project_id, forked_from_repository_storage_path, source_path, target_path) + project = Project.find(project_id) + + return unless start_fork(project) + Gitlab::Metrics.add_event(:fork_repository, source_path: source_path, target_path: target_path) - project = Project.find(project_id) - project.import_start - result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_path, project.repository_storage_path, target_path) raise ForkError, "Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}" unless result @@ -33,6 +36,13 @@ class RepositoryForkWorker private + def start_fork(project) + return true if project.import_start + + Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while forking.") + false + end + def fail_fork(project, message) Rails.logger.error(message) project.mark_import_as_failed(message) diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index 6be541abd3e..2c2d1e8b91f 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -4,23 +4,18 @@ class RepositoryImportWorker include Sidekiq::Worker include DedicatedSidekiqQueue - sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_EXPIRATION - - attr_accessor :project, :current_user + sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION def perform(project_id) - @project = Project.find(project_id) - @current_user = @project.creator + project = Project.find(project_id) - project.import_start + return unless start_import(project) Gitlab::Metrics.add_event(:import_repository, - import_url: @project.import_url, - path: @project.full_path) - - project.update_columns(import_jid: self.jid, import_error: nil) + import_url: project.import_url, + path: project.full_path) - result = Projects::ImportService.new(project, current_user).execute + result = Projects::ImportService.new(project, project.creator).execute raise ImportError, result[:message] if result[:status] == :error project.repository.after_import @@ -37,6 +32,13 @@ class RepositoryImportWorker private + def start_import(project) + return true if project.import_start + + Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.") + false + end + def fail_import(project, message) project.mark_import_as_failed(message) end diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb index bfc5e667bb6..f850e459cd9 100644 --- a/app/workers/stuck_import_jobs_worker.rb +++ b/app/workers/stuck_import_jobs_worker.rb @@ -2,36 +2,60 @@ class StuckImportJobsWorker include Sidekiq::Worker include CronjobQueue - IMPORT_EXPIRATION = 15.hours.to_i + IMPORT_JOBS_EXPIRATION = 15.hours.to_i def perform - stuck_projects.find_in_batches(batch_size: 500) do |group| + projects_without_jid_count = mark_projects_without_jid_as_failed! + projects_with_jid_count = mark_projects_with_jid_as_failed! + + Gitlab::Metrics.add_event(:stuck_import_jobs, + projects_without_jid_count: projects_without_jid_count, + projects_with_jid_count: projects_with_jid_count) + end + + private + + def mark_projects_without_jid_as_failed! + started_projects_without_jid.each do |project| + project.mark_import_as_failed(error_message) + end.count + end + + def mark_projects_with_jid_as_failed! + completed_jids_count = 0 + + started_projects_with_jid.find_in_batches(batch_size: 500) do |group| jids = group.map(&:import_jid) # Find the jobs that aren't currently running or that exceeded the threshold. - completed_jids = Gitlab::SidekiqStatus.completed_jids(jids) + completed_jids = Gitlab::SidekiqStatus.completed_jids(jids).to_set if completed_jids.any? - completed_ids = group.select { |project| completed_jids.include?(project.import_jid) }.map(&:id) + 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 - private + completed_jids_count + end - def stuck_projects - Project.select('id, import_jid').with_import_status(:started).where.not(import_jid: nil) + def started_projects + Project.with_import_status(:started) end - def fail_batch!(completed_jids, completed_ids) - Project.where(id: completed_ids).update_all(import_status: 'failed', import_error: error_message) + def started_projects_with_jid + started_projects.where.not(import_jid: nil) + end - Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_jids.join(', ')}") + def started_projects_without_jid + started_projects.where(import_jid: nil) end def error_message - "Import timed out. Import took longer than #{IMPORT_EXPIRATION} seconds" + "Import timed out. Import took longer than #{IMPORT_JOBS_EXPIRATION} seconds" end end |