diff options
Diffstat (limited to 'app')
46 files changed, 406 insertions, 228 deletions
diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js index 1c379e9bb67..7ac9dcd1112 100644 --- a/app/assets/javascripts/gpg_badges.js +++ b/app/assets/javascripts/gpg_badges.js @@ -1,12 +1,14 @@ export default class GpgBadges { static fetch() { + const badges = $('.js-loading-gpg-badge'); const form = $('.commits-search-form'); + badges.html('<i class="fa fa-spinner fa-spin"></i>'); + $.get({ url: form.data('signatures-path'), data: form.serialize(), }).done((response) => { - const badges = $('.js-loading-gpg-badge'); response.signatures.forEach((signature) => { badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html); }); diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js deleted file mode 100644 index c827b7402dc..00000000000 --- a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.js +++ /dev/null @@ -1,50 +0,0 @@ -import Vue from 'vue'; -import Cookies from 'js-cookie'; -import Translate from '../../vue_shared/translate'; -import illustrationSvg from '../icons/intro_illustration.svg'; - -Vue.use(Translate); - -const cookieKey = 'pipeline_schedules_callout_dismissed'; - -export default { - name: 'PipelineSchedulesCallout', - data() { - return { - docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl, - illustrationSvg, - calloutDismissed: Cookies.get(cookieKey) === 'true', - }; - }, - methods: { - dismissCallout() { - this.calloutDismissed = true; - Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 }); - }, - }, - template: ` - <div v-if="!calloutDismissed" class="pipeline-schedules-user-callout user-callout"> - <div class="bordered-box landing content-block"> - <button - id="dismiss-callout-btn" - class="btn btn-default close" - @click="dismissCallout"> - <i class="fa fa-times"></i> - </button> - <div class="svg-container" v-html="illustrationSvg"></div> - <div class="user-callout-copy"> - <h4>{{ __('Scheduling Pipelines') }}</h4> - <p> - {{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }} - </p> - <p> {{ __('Learn more in the') }} - <a - :href="docsUrl" - target="_blank" - rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period --> - </p> - </div> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue new file mode 100644 index 00000000000..6e0bc2d697a --- /dev/null +++ b/app/assets/javascripts/pipeline_schedules/components/pipeline_schedules_callout.vue @@ -0,0 +1,59 @@ +<script> + import Vue from 'vue'; + import Cookies from 'js-cookie'; + import Translate from '../../vue_shared/translate'; + import illustrationSvg from '../icons/intro_illustration.svg'; + + Vue.use(Translate); + + const cookieKey = 'pipeline_schedules_callout_dismissed'; + + export default { + name: 'PipelineSchedulesCallout', + data() { + return { + docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl, + calloutDismissed: Cookies.get(cookieKey) === 'true', + }; + }, + methods: { + dismissCallout() { + this.calloutDismissed = true; + Cookies.set(cookieKey, this.calloutDismissed, { expires: 365 }); + }, + }, + created() { + this.illustrationSvg = illustrationSvg; + }, + }; +</script> +<template> + <div + v-if="!calloutDismissed" + class="pipeline-schedules-user-callout user-callout"> + <div class="bordered-box landing content-block"> + <button + id="dismiss-callout-btn" + class="btn btn-default close" + @click="dismissCallout"> + <i + aria-hidden="true" + class="fa fa-times"> + </i> + </button> + <div class="svg-container" v-html="illustrationSvg"></div> + <div class="user-callout-copy"> + <h4>{{ __('Scheduling Pipelines') }}</h4> + <p> + {{ __('The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.') }} + </p> + <p> {{ __('Learn more in the') }} + <a + :href="docsUrl" + target="_blank" + rel="nofollow">{{ s__('Learn more in the|pipeline schedules documentation') }}</a>. <!-- oneline to prevent extra space before period --> + </p> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js b/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js index 6584549ad06..a6c945e22b0 100644 --- a/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js +++ b/app/assets/javascripts/pipeline_schedules/pipeline_schedules_index_bundle.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import PipelineSchedulesCallout from './components/pipeline_schedules_callout'; +import PipelineSchedulesCallout from './components/pipeline_schedules_callout.vue'; document.addEventListener('DOMContentLoaded', () => new Vue({ el: '#pipeline-schedules-callout', diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue index 2944689a5a7..7695b04db74 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -48,6 +48,27 @@ return `${this.job.name} - ${this.job.status.label}`; }, }, + + methods: { + /** + * When the user right clicks or cmd/ctrl + click in the job name + * the dropdown should not be closed and the link should open in another tab, + * so we stop propagation of the click event inside the dropdown. + * + * Since this component is rendered multiple times per page we need to guarantee we only + * target the click event of this component. + */ + stopDropdownClickPropagation() { + $(this.$el.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item')) + .on('click', (e) => { + e.stopPropagation(); + }); + }, + }, + + mounted() { + this.stopDropdownClickPropagation(); + }, }; </script> <template> diff --git a/app/assets/javascripts/repo/components/repo_edit_button.vue b/app/assets/javascripts/repo/components/repo_edit_button.vue index e954fd38fc9..f47b6c33fa2 100644 --- a/app/assets/javascripts/repo/components/repo_edit_button.vue +++ b/app/assets/javascripts/repo/components/repo_edit_button.vue @@ -29,12 +29,10 @@ export default { editMode() { if (this.editMode) { $('.project-refs-form').addClass('disabled'); - $('.fa-long-arrow-right').show(); - $('.project-refs-target-form').show(); + $('.js-tree-ref-target-holder').show(); } else { $('.project-refs-form').removeClass('disabled'); - $('.fa-long-arrow-right').hide(); - $('.project-refs-target-form').hide(); + $('.js-tree-ref-target-holder').hide(); } }, }, diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue index d8de022335b..2200754cbef 100644 --- a/app/assets/javascripts/repo/components/repo_preview.vue +++ b/app/assets/javascripts/repo/components/repo_preview.vue @@ -4,7 +4,7 @@ import Store from '../stores/repo_store'; export default { data: () => Store, mounted() { - $(this.$el).find('.file-content').syntaxHighlight(); + this.highlightFile(); }, computed: { html() { @@ -12,10 +12,16 @@ export default { }, }, + methods: { + highlightFile() { + $(this.$el).find('.file-content').syntaxHighlight(); + }, + }, + watch: { html() { this.$nextTick(() => { - $(this.$el).find('.file-content').syntaxHighlight(); + this.highlightFile(); }); }, }, @@ -24,9 +30,23 @@ export default { <template> <div> - <div v-if="!activeFile.render_error" v-html="activeFile.html"></div> - <div v-if="activeFile.render_error" class="vertical-center render-error"> - <p class="text-center">The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.</p> + <div + v-if="!activeFile.render_error" + v-html="activeFile.html"> + </div> + <div + v-else-if="activeFile.tooLarge" + class="vertical-center render-error"> + <p class="text-center"> + The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead. + </p> + </div> + <div + v-else + class="vertical-center render-error"> + <p class="text-center"> + The source could not be displayed because a rendering error occured. You can <a :href="activeFile.raw_path">download</a> it instead. + </p> </div> </div> </template> diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue index d6d832efc49..0d4f8c6635e 100644 --- a/app/assets/javascripts/repo/components/repo_sidebar.vue +++ b/app/assets/javascripts/repo/components/repo_sidebar.vue @@ -33,32 +33,30 @@ const RepoSidebar = { }); }, - linkClicked(clickedFile) { - let url = ''; + fileClicked(clickedFile) { let file = clickedFile; - if (typeof file === 'object') { - file.loading = true; - if (file.type === 'tree' && file.opened) { - file = Store.removeChildFilesOfTree(file); - file.loading = false; - } else { - url = file.url; - Service.url = url; - // I need to refactor this to do the `then` here. - // Not a callback. For now this is good enough. - // it works. - Helper.getContent(file, () => { + + file.loading = true; + if (file.type === 'tree' && file.opened) { + file = Store.removeChildFilesOfTree(file); + file.loading = false; + } else { + Service.url = file.url; + Helper.getContent(file) + .then(() => { file.loading = false; Helper.scrollTabsRight(); - }); - } - } else if (typeof file === 'string') { - // go back - url = file; - Service.url = url; - Helper.getContent(null, () => Helper.scrollTabsRight()); + }) + .catch(Helper.loadingError); } }, + + goToPreviousDirectoryClicked(prevURL) { + Service.url = prevURL; + Helper.getContent(null) + .then(() => Helper.scrollTabsRight()) + .catch(Helper.loadingError); + }, }, }; @@ -82,7 +80,7 @@ export default RepoSidebar; <repo-previous-directory v-if="isRoot" :prev-url="prevURL" - @linkclicked="linkClicked(prevURL)"/> + @linkclicked="goToPreviousDirectoryClicked(prevURL)"/> <repo-loading-file v-for="n in 5" :key="n" @@ -94,7 +92,7 @@ export default RepoSidebar; :key="file.id" :file="file" :is-mini="isMini" - @linkclicked="linkClicked(file)" + @linkclicked="fileClicked(file)" :is-tree="isTree" :has-files="!!files.length" :active-file="activeFile"/> diff --git a/app/assets/javascripts/repo/components/repo_tab.vue b/app/assets/javascripts/repo/components/repo_tab.vue index 712d64c236f..fc66a8ea953 100644 --- a/app/assets/javascripts/repo/components/repo_tab.vue +++ b/app/assets/javascripts/repo/components/repo_tab.vue @@ -10,6 +10,12 @@ const RepoTab = { }, computed: { + closeLabel() { + if (this.tab.changed) { + return `${this.tab.name} changed`; + } + return `Close ${this.tab.name}`; + }, changedClass() { const tabChangedObj = { 'fa-times': !this.tab.changed, @@ -34,12 +40,24 @@ export default RepoTab; <template> <li> - <a href="#" class="close" @click.prevent="xClicked(tab)" v-if="!tab.loading"> - <i class="fa" :class="changedClass"></i> + <a + href="#0" + class="close" + @click.prevent="xClicked(tab)" + :aria-label="closeLabel"> + <i + class="fa" + :class="changedClass" + aria-hidden="true"> + </i> </a> - <a href="#" class="repo-tab" v-if="!tab.loading" :title="tab.url" @click.prevent="tabClicked(tab)">{{tab.name}}</a> - - <i v-if="tab.loading" class="fa fa-spinner fa-spin"></i> + <a + href="#" + class="repo-tab" + :title="tab.url" + @click.prevent="tabClicked(tab)"> + {{tab.name}} + </a> </li> </template> diff --git a/app/assets/javascripts/repo/components/repo_tabs.vue b/app/assets/javascripts/repo/components/repo_tabs.vue index 907a03e1601..bbd60d9d793 100644 --- a/app/assets/javascripts/repo/components/repo_tabs.vue +++ b/app/assets/javascripts/repo/components/repo_tabs.vue @@ -1,5 +1,4 @@ <script> -import Vue from 'vue'; import Store from '../stores/repo_store'; import RepoTab from './repo_tab.vue'; import RepoMixin from '../mixins/repo_mixin'; @@ -14,29 +13,19 @@ const RepoTabs = { data: () => Store, methods: { - isOverflow() { - return this.$el.scrollWidth > this.$el.offsetWidth; - }, - xClicked(file) { Store.removeFromOpenedFiles(file); }, }, - - watch: { - openedFiles() { - Vue.nextTick(() => { - this.tabsOverflow = this.isOverflow(); - }); - }, - }, }; export default RepoTabs; </script> <template> -<ul id="tabs" v-if="isMini" v-cloak :class="{'overflown': tabsOverflow}"> +<ul + v-if="isMini" + id="tabs"> <repo-tab v-for="tab in openedFiles" :key="tab.id" :tab="tab" :class="{'active' : tab.active}" @xclicked="xClicked"/> <li class="tabs-divider" /> </ul> diff --git a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js b/app/assets/javascripts/repo/helpers/monaco_loader_helper.js index 8ee2df5c879..c1a0e80f8f3 100644 --- a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js +++ b/app/assets/javascripts/repo/helpers/monaco_loader_helper.js @@ -10,7 +10,10 @@ function repoEditorLoader() { Store.monaco = monaco; Store.monacoLoading = false; resolve(RepoEditor); - }, reject); + }, () => { + Store.monacoLoading = false; + reject(); + }); }); } diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js index fee98c12592..17aaa0e1584 100644 --- a/app/assets/javascripts/repo/helpers/repo_helper.js +++ b/app/assets/javascripts/repo/helpers/repo_helper.js @@ -33,12 +33,16 @@ const RepoHelper = { ? window.performance : Date, + getFileExtension(fileName) { + return fileName.split('.').pop(); + }, + getBranch() { return $('button.dropdown-menu-toggle').attr('data-ref'); }, getLanguageIDForFile(file, langs) { - const ext = file.name.split('.').pop(); + const ext = RepoHelper.getFileExtension(file.name); const foundLang = RepoHelper.findLanguage(ext, langs); return foundLang ? foundLang.id : 'plaintext'; @@ -135,21 +139,19 @@ const RepoHelper = { return isRoot; }, - getContent(treeOrFile, cb) { + getContent(treeOrFile) { let file = treeOrFile; // const loadingData = RepoHelper.setLoading(true); return Service.getContent() .then((response) => { const data = response.data; // RepoHelper.setLoading(false, loadingData); - if (cb) cb(); Store.isTree = RepoHelper.isTree(data); if (!Store.isTree) { if (!file) file = data; Store.binary = data.binary; if (data.binary) { - Store.binaryMimeType = data.mime_type; // file might be undefined RepoHelper.setBinaryDataAsBase64(data); Store.setViewToPreview(); @@ -188,9 +190,8 @@ const RepoHelper = { setFile(data, file) { const newFile = data; - newFile.url = file.url || location.pathname; newFile.url = file.url; - if (newFile.render_error === 'too_large') { + if (newFile.render_error === 'too_large' || newFile.render_error === 'collapsed') { newFile.tooLarge = true; } newFile.newContent = ''; @@ -199,10 +200,6 @@ const RepoHelper = { Store.setActiveFiles(newFile); }, - toFA(icon) { - return `fa-${icon}`; - }, - serializeBlob(blob) { const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob); simpleBlob.lastCommitMessage = blob.last_commit.message; @@ -226,7 +223,7 @@ const RepoHelper = { type, name, url, - icon: RepoHelper.toFA(icon), + icon: `fa-${icon}`, level: 0, loading: false, }; @@ -244,7 +241,7 @@ const RepoHelper = { setTimeout(() => { const tabs = document.getElementById('tabs'); if (!tabs) return; - tabs.scrollLeft = 12000; + tabs.scrollLeft = tabs.scrollWidth; }, 200); }, diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js index 67c03680fca..3e37da1726e 100644 --- a/app/assets/javascripts/repo/index.js +++ b/app/assets/javascripts/repo/index.js @@ -7,8 +7,7 @@ import RepoEditButton from './components/repo_edit_button.vue'; import Translate from '../vue_shared/translate'; function initDropdowns() { - $('.project-refs-target-form').hide(); - $('.fa-long-arrow-right').hide(); + $('.js-tree-ref-target-holder').hide(); } function addEventsForNonVueEls() { diff --git a/app/assets/javascripts/repo/services/repo_service.js b/app/assets/javascripts/repo/services/repo_service.js index 8fba928e456..17578f3bbf3 100644 --- a/app/assets/javascripts/repo/services/repo_service.js +++ b/app/assets/javascripts/repo/services/repo_service.js @@ -2,6 +2,7 @@ import axios from 'axios'; import Store from '../stores/repo_store'; import Api from '../../api'; +import Helper from '../helpers/repo_helper'; const RepoService = { url: '', @@ -22,6 +23,7 @@ const RepoService = { getRaw(url) { return axios.get(url, { + // Stop Axios from parsing a JSON file into a JS object transformResponse: [res => res], }); }, @@ -36,7 +38,7 @@ const RepoService = { }, urlIsRichBlob(url = this.url) { - const extension = url.split('.').pop(); + const extension = Helper.getFileExtension(url); return this.richExtensionRegExp.test(extension); }, diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js index 06ca391ed0c..bb605540aad 100644 --- a/app/assets/javascripts/repo/stores/repo_store.js +++ b/app/assets/javascripts/repo/stores/repo_store.js @@ -3,13 +3,10 @@ import Helper from '../helpers/repo_helper'; import Service from '../services/repo_service'; const RepoStore = { - ideEl: {}, monaco: {}, monacoLoading: false, monacoInstance: {}, service: '', - editor: '', - sidebar: '', editMode: false, isTree: false, isRoot: false, @@ -17,19 +14,10 @@ const RepoStore = { projectId: '', projectName: '', projectUrl: '', - trees: [], - blobs: [], - submodules: [], blobRaw: '', - blobRendered: '', currentBlobView: 'repo-preview', openedFiles: [], - tabSize: 100, - defaultTabSize: 100, - minTabSize: 30, - tabsOverflow: 41, submitCommitsLoading: false, - binaryLoaded: false, dialog: { open: false, title: '', @@ -45,9 +33,6 @@ const RepoStore = { currentBranch: '', targetBranch: 'new-branch', commitMessage: '', - binaryMimeType: '', - // scroll bar space for windows - scrollWidth: 0, binaryTypes: { png: false, md: false, @@ -58,7 +43,6 @@ const RepoStore = { tree: false, blob: false, }, - readOnly: true, resetBinaryTypes() { Object.keys(RepoStore.binaryTypes).forEach((key) => { @@ -96,7 +80,6 @@ const RepoStore = { if (file.binary) { RepoStore.blobRaw = file.base64; - RepoStore.binaryMimeType = file.mime_type; } else if (file.newContent || file.plain) { RepoStore.blobRaw = file.newContent || file.plain; } else { @@ -238,4 +221,5 @@ const RepoStore = { return RepoStore.currentBlobView === 'repo-preview'; }, }; + export default RepoStore; 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 422c02c7b7e..cfacba09fad 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> - None + This issue is 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/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index cd9f2d787c5..46fbfe5f91e 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -286,6 +286,10 @@ .gpg-status-box { + &:empty { + display: none; + } + &.valid { @include green-status-color; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 2bb867052f6..0a194f3707f 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -453,7 +453,10 @@ ul.notes { } .note-actions { + align-self: flex-start; flex-shrink: 0; + display: inline-flex; + align-items: center; // For PhantomJS that does not support flex float: right; margin-left: 10px; @@ -463,18 +466,12 @@ ul.notes { float: none; margin-left: 0; } - - .note-action-button { - margin-left: 8px; - } - - .more-actions-toggle { - margin-left: 2px; - } } .more-actions { - display: inline-block; + float: right; // phantomjs fallback + display: flex; + align-items: flex-end; .tooltip { white-space: nowrap; @@ -482,16 +479,10 @@ ul.notes { } .more-actions-toggle { - padding: 0; - &:hover .icon, &:focus .icon { color: $blue-600; } - - .icon { - padding: 0 6px; - } } .more-actions-dropdown { @@ -519,28 +510,42 @@ ul.notes { @include notes-media('max', $screen-md-max) { float: none; margin-left: 0; + } +} - .note-action-button { - margin-left: 0; - } +.note-actions-item { + margin-left: 15px; + display: flex; + align-items: center; + + &.more-actions { + // compensate for narrow icon + margin-left: 10px; } } .note-action-button { - display: inline; - line-height: 20px; + line-height: 1; + padding: 0; + min-width: 16px; + color: $gray-darkest; .fa { - color: $gray-darkest; position: relative; - font-size: 17px; + font-size: 16px; } + + svg { height: 16px; width: 16px; - fill: $gray-darkest; + top: 0; vertical-align: text-top; + + path { + fill: currentColor; + } } .award-control-icon-positive, @@ -613,10 +618,7 @@ ul.notes { .note-role { position: relative; - top: -2px; - display: inline-block; - padding-left: 7px; - padding-right: 7px; + padding: 0 7px; color: $notes-role-color; font-size: 12px; line-height: 20px; diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 11236cbf2e7..0028e207f3e 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -29,6 +29,10 @@ margin-right: 15px; } + .tree-ref-target-holder { + display: inline-block; + } + .repo-breadcrumb { li:last-of-type { position: relative; diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb index 52e06f4945a..1ab107168c0 100644 --- a/app/controllers/concerns/cycle_analytics_params.rb +++ b/app/controllers/concerns/cycle_analytics_params.rb @@ -6,6 +6,13 @@ module CycleAnalyticsParams end def start_date(params) - params[:start_date] == '30' ? 30.days.ago : 90.days.ago + case params[:start_date] + when '7' + 7.days.ago + when '30' + 30.days.ago + else + 90.days.ago + end end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index f4d4cca8dd8..8893a514207 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -212,7 +212,7 @@ class Projects::IssuesController < Projects::ApplicationController end def create_merge_request - result = MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute + result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute if result[:status] == :success render json: MergeRequestCreateSerializer.new.represent(result[:merge_request]) diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 8cd61f738e1..4123a96911f 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -59,7 +59,7 @@ module GroupsHelper end def remove_group_message(group) - _("You are going to remove %{group_name}.\nRemoved groups CANNOT be restored!\nAre you ABSOLUTELY sure?") % + _("You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") % { group_name: group.name } end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index a268413e84f..09cfd06dad3 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -80,7 +80,7 @@ module ProjectsHelper end def remove_project_message(project) - _("You are going to remove %{project_name_with_namespace}.\nRemoved project CANNOT be restored!\nAre you ABSOLUTELY sure?") % + _("You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") % { project_name_with_namespace: project.name_with_namespace } end diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb index 7b617b359ea..d76c61c369f 100644 --- a/app/mailers/emails/members.rb +++ b/app/mailers/emails/members.rb @@ -11,11 +11,11 @@ module Emails @member_source_type = member_source_type @member_id = member_id - admins = member_source.members.owners_and_masters.includes(:user).pluck(:notification_email) + admins = member_source.members.owners_and_masters.pluck(:notification_email) # A project in a group can have no explicit owners/masters, in that case # we fallbacks to the group's owners/masters. if admins.empty? && member_source.respond_to?(:group) && member_source.group - admins = member_source.group.members.owners_and_masters.includes(:user).pluck(:notification_email) + admins = member_source.group.members.owners_and_masters.pluck(:notification_email) end mail(to: admins, diff --git a/app/models/group.rb b/app/models/group.rb index bd5735ed82e..2816a68257c 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -212,21 +212,39 @@ class Group < Namespace end def user_ids_for_project_authorizations - users_with_parents.pluck(:id) + members_with_parents.pluck(:user_id) end def members_with_parents - GroupMember.active.where(source_id: ancestors.pluck(:id).push(id)).where.not(user_id: nil) + # Avoids an unnecessary SELECT when the group has no parents + source_ids = + if parent_id + self_and_ancestors.reorder(nil).select(:id) + else + id + end + + GroupMember + .active_without_invites + .where(source_id: source_ids) + end + + def members_with_descendants + GroupMember + .active_without_invites + .where(source_id: self_and_descendants.reorder(nil).select(:id)) end def users_with_parents - User.where(id: members_with_parents.select(:user_id)) + User + .where(id: members_with_parents.select(:user_id)) + .reorder(nil) end def users_with_descendants - members_with_descendants = GroupMember.non_request.where(source_id: descendants.pluck(:id).push(id)) - - User.where(id: members_with_descendants.select(:user_id)) + User + .where(id: members_with_descendants.select(:user_id)) + .reorder(nil) end def max_member_access_for_user(user) diff --git a/app/models/member.rb b/app/models/member.rb index b26b5017183..ee2cb13697b 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -41,9 +41,20 @@ class Member < ActiveRecord::Base is_external_invite = arel_table[:user_id].eq(nil).and(arel_table[:invite_token].not_eq(nil)) user_is_active = User.arel_table[:state].eq(:active) - includes(:user).references(:users) - .where(is_external_invite.or(user_is_active)) + user_ok = Arel::Nodes::Grouping.new(is_external_invite).or(user_is_active) + + left_join_users + .where(user_ok) + .where(requested_at: nil) + .reorder(nil) + end + + # Like active, but without invites. For when a User is required. + scope :active_without_invites, -> do + left_join_users + .where(users: { state: 'active' }) .where(requested_at: nil) + .reorder(nil) end scope :invite, -> { where.not(invite_token: nil) } diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index f90194041b1..ac08dc0ee1f 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -443,7 +443,8 @@ class MergeRequest < ActiveRecord::Base end def reload_diff_if_branch_changed - if source_branch_changed? || target_branch_changed? + if (source_branch_changed? || target_branch_changed?) && + (source_branch_head && target_branch_head) reload_diff end end @@ -792,11 +793,7 @@ class MergeRequest < ActiveRecord::Base end def fetch_ref - target_project.repository.fetch_ref( - source_project.repository.path_to_repo, - "refs/heads/#{source_branch}", - ref_path - ) + write_ref update_column(:ref_fetched, true) end @@ -939,4 +936,17 @@ class MergeRequest < ActiveRecord::Base true end + + private + + def write_ref + target_project.repository.with_repo_branch_commit( + source_project.repository, source_branch) do |commit| + if commit + target_project.repository.write_ref(ref_path, commit.sha) + else + raise Rugged::ReferenceError, 'source repository is empty' + end + end + end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 6073fb94a3f..e7bc1d1b080 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -156,6 +156,14 @@ class Namespace < ActiveRecord::Base .base_and_ancestors end + def self_and_ancestors + return self.class.where(id: id) unless parent_id + + Gitlab::GroupHierarchy + .new(self.class.where(id: id)) + .base_and_ancestors + end + # Returns all the descendants of the current namespace. def descendants Gitlab::GroupHierarchy @@ -163,6 +171,12 @@ class Namespace < ActiveRecord::Base .base_and_descendants end + def self_and_descendants + Gitlab::GroupHierarchy + .new(self.class.where(id: id)) + .base_and_descendants + end + def user_ids_for_project_authorizations [owner_id] end diff --git a/app/models/project.rb b/app/models/project.rb index 7010664e1c8..0de7da0ddaa 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -196,7 +196,6 @@ class Project < ActiveRecord::Base accepts_nested_attributes_for :import_data delegate :name, to: :owner, allow_nil: true, prefix: true - delegate :count, to: :forks, prefix: true delegate :members, to: :team, prefix: true delegate :add_user, :add_users, to: :team delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team @@ -1048,9 +1047,7 @@ class Project < ActiveRecord::Base def change_head(branch) if repository.branch_exists?(branch) repository.before_change_head - repository.rugged.references.create('HEAD', - "refs/heads/#{branch}", - force: true) + repository.write_ref('HEAD', "refs/heads/#{branch}") repository.copy_gitattributes(branch) repository.after_change_head reload_default_branch @@ -1398,6 +1395,10 @@ class Project < ActiveRecord::Base # @deprecated cannot remove yet because it has an index with its name in elasticsearch alias_method :path_with_namespace, :full_path + def forks_count + Projects::ForksCountService.new(self).count + end + private def cross_namespace_reference?(from) diff --git a/app/models/repository.rb b/app/models/repository.rb index 049bebdbe42..a761302b06b 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -224,7 +224,7 @@ class Repository # This will still fail if the file is corrupted (e.g. 0 bytes) begin - rugged.references.create(keep_around_ref_name(sha), sha, force: true) + write_ref(keep_around_ref_name(sha), sha) rescue Rugged::ReferenceError => ex Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}" rescue Rugged::OSError => ex @@ -237,6 +237,10 @@ class Repository ref_exists?(keep_around_ref_name(sha)) end + def write_ref(ref_path, sha) + rugged.references.create(ref_path, sha, force: true) + end + def diverging_commit_counts(branch) root_ref_hash = raw_repository.rev_parse_target(root_ref).oid cache.fetch(:"diverging_commit_counts_#{branch.name}") do @@ -985,12 +989,10 @@ class Repository if start_repository == self start_branch_name else - tmp_ref = "refs/tmp/#{SecureRandom.hex}/head" - - fetch_ref( + tmp_ref = fetch_ref( start_repository.path_to_repo, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}", - tmp_ref + "refs/tmp/#{SecureRandom.hex}/head" ) start_repository.commit(start_branch_name).sha @@ -1021,7 +1023,12 @@ class Repository def fetch_ref(source_path, source_ref, target_ref) args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) - run_git(args) + message, status = run_git(args) + + # Make sure ref was created, and raise Rugged::ReferenceError when not + raise Rugged::ReferenceError, message if status != 0 + + target_ref end def create_ref(ref, ref_path) diff --git a/app/models/user.rb b/app/models/user.rb index a4615436245..2b25736bb26 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -726,9 +726,9 @@ class User < ActiveRecord::Base end def sanitize_attrs - %w[username skype linkedin twitter].each do |attr| - value = public_send(attr) # rubocop:disable GitlabSecurity/PublicSend - public_send("#{attr}=", Sanitize.clean(value)) if value.present? # rubocop:disable GitlabSecurity/PublicSend + %i[skype linkedin twitter].each do |attr| + value = self[attr] + self[attr] = Sanitize.clean(value) if value.present? end end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 11ad4838471..54eb75ab9bf 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -128,6 +128,8 @@ module Projects project.repository.before_delete Repository.new(wiki_path, project, disk_path: repo_path).before_delete + + Projects::ForksCountService.new(project).delete_cache end end end diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index a2b23ea6171..ad67e68a86a 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -21,11 +21,17 @@ module Projects builds_access_level = @project.project_feature.builds_access_level new_project.project_feature.update_attributes(builds_access_level: builds_access_level) + refresh_forks_count + new_project end private + def refresh_forks_count + Projects::ForksCountService.new(@project).refresh_cache + end + def allowed_visibility_level project_level = @project.visibility_level diff --git a/app/services/projects/forks_count_service.rb b/app/services/projects/forks_count_service.rb new file mode 100644 index 00000000000..e2e2b1da91d --- /dev/null +++ b/app/services/projects/forks_count_service.rb @@ -0,0 +1,30 @@ +module Projects + # Service class for getting and caching the number of forks of a project. + class ForksCountService + def initialize(project) + @project = project + end + + def count + Rails.cache.fetch(cache_key) { uncached_count } + end + + def refresh_cache + Rails.cache.write(cache_key, uncached_count) + end + + def delete_cache + Rails.cache.delete(cache_key) + end + + private + + def uncached_count + @project.forks.count + end + + def cache_key + ['projects', @project.id, 'forks_count'] + end + end +end diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb index f385e426827..f30b40423c8 100644 --- a/app/services/projects/unlink_fork_service.rb +++ b/app/services/projects/unlink_fork_service.rb @@ -13,7 +13,13 @@ module Projects ::MergeRequests::CloseService.new(@project, @current_user).execute(mr) end + refresh_forks_count(@project.forked_from_project) + @project.forked_project_link.destroy end + + def refresh_forks_count(project) + Projects::ForksCountService.new(project).refresh_cache + end end end diff --git a/app/views/projects/commit/_ajax_signature.html.haml b/app/views/projects/commit/_ajax_signature.html.haml index 22674b671c9..83821326aec 100644 --- a/app/views/projects/commit/_ajax_signature.html.haml +++ b/app/views/projects/commit/_ajax_signature.html.haml @@ -1,3 +1,2 @@ - if commit.has_signature? %button{ class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'auto top', title: 'GPG signature (loading...)', 'commit-sha' => commit.sha } } - %i.fa.fa-spinner.fa-spin diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index c704635ead3..3467e357c49 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -40,6 +40,9 @@ %i.fa.fa-chevron-down %ul.dropdown-menu.dropdown-menu-align-right %li + %a{ "href" => "#", "data-value" => "7" } + {{ n__('Last %d day', 'Last %d days', 7) }} + %li %a{ "href" => "#", "data-value" => "30" } {{ n__('Last %d day', 'Last %d days', 30) }} %li diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml index 756faf4625e..13809da6523 100644 --- a/app/views/projects/issues/_nav_btns.html.haml +++ b/app/views/projects/issues/_nav_btns.html.haml @@ -1,7 +1,7 @@ = link_to params.merge(rss_url_options), class: 'btn btn-default append-right-10 has-tooltip', title: 'Subscribe' do = icon('rss') - if @can_bulk_update - = button_tag "Edit Issues", class: "btn btn-default append-right-10 js-bulk-update-toggle" + = button_tag "Edit issues", class: "btn btn-default append-right-10 js-bulk-update-toggle" = link_to "New issue", new_project_issue_path(@project, issue: { assignee_id: issues_finder.assignee.try(:id), milestone_id: issues_finder.milestones.first.try(:id) }), diff --git a/app/views/projects/merge_requests/_nav_btns.html.haml b/app/views/projects/merge_requests/_nav_btns.html.haml index 60258a7a39d..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 append-right-10 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/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml index 9c42be4e0ff..cb737d129f0 100644 --- a/app/views/projects/notes/_actions.html.haml +++ b/app/views/projects/notes/_actions.html.haml @@ -17,24 +17,32 @@ "inline-template" => true, "ref" => "note_#{note.id}" } - %button.note-action-button.line-resolve-btn{ type: "button", - class: ("is-disabled" unless can_resolve), - ":class" => "{ 'is-active': isResolved }", - ":aria-label" => "buttonText", - "@click" => "resolve", - ":title" => "buttonText", - ":ref" => "'button'" } + .note-actions-item + %button.note-action-button.line-resolve-btn{ type: "button", + class: ("is-disabled" unless can_resolve), + ":class" => "{ 'is-active': isResolved }", + ":aria-label" => "buttonText", + "@click" => "resolve", + ":title" => "buttonText", + ":ref" => "'button'" } - = icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading') - %div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg' + = icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading') + %div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg' - if current_user - if note.emoji_awardable? - user_authored = note.user_authored?(current_user) - = link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do - = icon('spinner spin') - %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') - %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') - %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') + .note-actions-item + = button_tag title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip btn btn-transparent", data: { position: 'right', container: 'body' } do + = icon('spinner spin') + %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') + %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') + %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') - = render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable + - if note_editable + .note-actions-item + = button_tag title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip btn btn-transparent', data: { container: 'body' } do + %span.link-highlight + = custom_icon('icon_pencil') + + = render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml index 75a4687e1e3..5930209a682 100644 --- a/app/views/projects/notes/_more_actions_dropdown.html.haml +++ b/app/views/projects/notes/_more_actions_dropdown.html.haml @@ -1,14 +1,11 @@ - is_current_user = current_user == note.author - if note_editable || !is_current_user - .dropdown.more-actions + .dropdown.more-actions.note-actions-item = button_tag title: 'More actions', class: 'note-action-button more-actions-toggle has-tooltip btn btn-transparent', data: { toggle: 'dropdown', container: 'body' } do - = icon('ellipsis-v', class: 'icon') + %span.icon + = custom_icon('ellipsis_v') %ul.dropdown-menu.more-actions-dropdown.dropdown-open-left - - if note_editable - %li - = button_tag 'Edit comment', class: 'js-note-edit btn btn-transparent' - %li.divider - unless is_current_user %li = link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 427b059cb82..853e2a6e7ec 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -2,8 +2,9 @@ .tree-ref-holder = render 'shared/ref_switcher', destination: 'tree', path: @path - if show_new_repo? - = icon('long-arrow-right', title: 'to target branch') - = render 'shared/target_switcher', destination: 'tree', path: @path + .tree-ref-target-holder.js-tree-ref-target-holder + = icon('long-arrow-right', title: 'to target branch') + = render 'shared/target_switcher', destination: 'tree', path: @path - unless show_new_repo? = render 'projects/tree/old_tree_header' diff --git a/app/views/shared/icons/_ellipsis_v.svg b/app/views/shared/icons/_ellipsis_v.svg new file mode 100644 index 00000000000..9117a9bb9ec --- /dev/null +++ b/app/views/shared/icons/_ellipsis_v.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 1600"><path d="M1088 1248v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V736q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V224q0-40 28-68t68-28h192q40 0 68 28t28 68z"/></svg> diff --git a/app/views/shared/icons/_node_express.svg b/app/views/shared/icons/_express.svg index f2c94319f19..f2c94319f19 100644 --- a/app/views/shared/icons/_node_express.svg +++ b/app/views/shared/icons/_express.svg diff --git a/app/views/shared/icons/_java_spring.svg b/app/views/shared/icons/_spring.svg index 508349aa456..508349aa456 100644 --- a/app/views/shared/icons/_java_spring.svg +++ b/app/views/shared/icons/_spring.svg diff --git a/app/views/snippets/notes/_actions.html.haml b/app/views/snippets/notes/_actions.html.haml index 098a88c48c5..3a50324770d 100644 --- a/app/views/snippets/notes/_actions.html.haml +++ b/app/views/snippets/notes/_actions.html.haml @@ -1,10 +1,17 @@ - if current_user - if note.emoji_awardable? - user_authored = note.user_authored?(current_user) - = link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do - = icon('spinner spin') - %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') - %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') - %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') + .note-actions-item + = link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do + = icon('spinner spin') + %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') + %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') + %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') + + - if note_editable + .note-actions-item + = button_tag title: 'Edit comment', class: 'note-action-button js-note-edit has-tooltip btn btn-transparent', data: { container: 'body' } do + %span.link-highlight + = custom_icon('icon_pencil') = render 'projects/notes/more_actions_dropdown', note: note, note_editable: note_editable |
