diff options
Diffstat (limited to 'app')
37 files changed, 269 insertions, 246 deletions
diff --git a/app/assets/javascripts/clone_panel.js b/app/assets/javascripts/clone_panel.js index 00bf54e1478..c9fae8f17a4 100644 --- a/app/assets/javascripts/clone_panel.js +++ b/app/assets/javascripts/clone_panel.js @@ -18,6 +18,11 @@ export default function initClonePanel() { e.preventDefault(); const $this = $(e.currentTarget); const url = $this.attr('href'); + if (url && (url.startsWith('vscode://') || url.startsWith('xcode://'))) { + // Clone with "..." should open like a normal link + return; + } + e.preventDefault(); const cloneType = $this.data('cloneType'); $('.is-active', $cloneOptions).removeClass('is-active'); diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue index bd8d2d6b8f2..6b1e2bfb34e 100644 --- a/app/assets/javascripts/diffs/components/compare_versions.vue +++ b/app/assets/javascripts/diffs/components/compare_versions.vue @@ -1,7 +1,6 @@ <script> import { GlTooltipDirective, GlLink, GlButton, GlSprintf } from '@gitlab/ui'; import { mapActions, mapGetters, mapState } from 'vuex'; -import { polyfillSticky } from '~/lib/utils/sticky'; import { __ } from '~/locale'; import { CENTERED_LIMITED_CONTAINER_CLASSES, EVT_EXPAND_ALL_FILES } from '../constants'; import eventHub from '../event_hub'; @@ -61,9 +60,6 @@ export default { created() { this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES; }, - mounted() { - polyfillSticky(this.$el); - }, methods: { ...mapActions('diffs', ['setInlineDiffViewType', 'setParallelDiffViewType', 'setShowTreeList']), expandAllFiles() { diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index 6bd74b143e2..ed6b750480b 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -107,3 +107,7 @@ export const SIDE_RIGHT = 'right'; // Live Preview feature export const LIVE_PREVIEW_DEBOUNCE = 2000; + +// This is the maximum number of files to auto open when opening the Web IDE +// from a Merge Request +export const MAX_MR_FILES_AUTO_OPEN = 10; diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index aa2e3d32b59..d1e40920ebc 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -120,10 +120,6 @@ export const getFileData = ( }); }; -export const setFileMrChange = ({ commit }, { file, mrChange }) => { - commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange }); -}; - export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) => { const file = state.entries[path]; const stagedFile = state.stagedFiles.find((f) => f.path === path); diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js index 8dcc420f156..753f6b9cd47 100644 --- a/app/assets/javascripts/ide/stores/actions/merge_request.js +++ b/app/assets/javascripts/ide/stores/actions/merge_request.js @@ -1,6 +1,6 @@ import { deprecatedCreateFlash as flash } from '~/flash'; import { __ } from '~/locale'; -import { leftSidebarViews, PERMISSION_READ_MR } from '../../constants'; +import { leftSidebarViews, PERMISSION_READ_MR, MAX_MR_FILES_AUTO_OPEN } from '../../constants'; import service from '../../services'; import * as types from '../mutation_types'; @@ -147,70 +147,96 @@ export const getMergeRequestVersions = ( } }); -export const openMergeRequest = ( - { dispatch, state, getters }, - { projectId, targetProjectId, mergeRequestId } = {}, -) => - dispatch('getMergeRequestData', { - projectId, - targetProjectId, - mergeRequestId, - }) - .then((mr) => { - dispatch('setCurrentBranchId', mr.source_branch); - - return dispatch('getBranchData', { - projectId, - branchId: mr.source_branch, - }).then(() => { - const branch = getters.findBranch(projectId, mr.source_branch); - - return dispatch('getFiles', { - projectId, - branchId: mr.source_branch, - ref: branch.commit.id, +export const openMergeRequestChanges = async ({ dispatch, getters, state, commit }, changes) => { + const entryChanges = changes + .map((change) => ({ entry: state.entries[change.new_path], change })) + .filter((x) => x.entry); + + const pathsToOpen = entryChanges + .slice(0, MAX_MR_FILES_AUTO_OPEN) + .map(({ change }) => change.new_path); + + // If there are no changes with entries, do nothing. + if (!entryChanges.length) { + return; + } + + dispatch('updateActivityBarView', leftSidebarViews.review.name); + + entryChanges.forEach(({ change, entry }) => { + commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file: entry, mrChange: change }); + }); + + // Open paths in order as they appear in MR changes + pathsToOpen.forEach((path) => { + commit(types.TOGGLE_FILE_OPEN, path); + }); + + // Activate first path. + // We don't `getFileData` here since the editor component kicks that off. Otherwise, we'd fetch twice. + const [firstPath, ...remainingPaths] = pathsToOpen; + await dispatch('router/push', getters.getUrlForPath(firstPath)); + await dispatch('setFileActive', firstPath); + + // Lastly, eagerly fetch the remaining paths for improved user experience. + await Promise.all( + remainingPaths.map(async (path) => { + try { + await dispatch('getFileData', { + path, + makeFileActive: false, }); - }); - }) - .then(() => - dispatch('getMergeRequestVersions', { - projectId, - targetProjectId, - mergeRequestId, - }), - ) - .then(() => - dispatch('getMergeRequestChanges', { - projectId, - targetProjectId, - mergeRequestId, - }), - ) - .then((mrChanges) => { - if (mrChanges.changes.length) { - dispatch('updateActivityBarView', leftSidebarViews.review.name); + await dispatch('getRawFileData', { path }); + } catch (e) { + // If one of the file fetches fails, we dont want to blow up the rest of them. + // eslint-disable-next-line no-console + console.error('[gitlab] An unexpected error occurred fetching MR file data', e); } + }), + ); +}; - mrChanges.changes.forEach((change, ind) => { - const changeTreeEntry = state.entries[change.new_path]; +export const openMergeRequest = async ( + { dispatch, getters }, + { projectId, targetProjectId, mergeRequestId } = {}, +) => { + try { + const mr = await dispatch('getMergeRequestData', { + projectId, + targetProjectId, + mergeRequestId, + }); - if (changeTreeEntry) { - dispatch('setFileMrChange', { - file: changeTreeEntry, - mrChange: change, - }); + dispatch('setCurrentBranchId', mr.source_branch); - if (ind < 10) { - dispatch('getFileData', { - path: change.new_path, - makeFileActive: ind === 0, - openFile: true, - }); - } - } - }); - }) - .catch((e) => { - flash(__('Error while loading the merge request. Please try again.')); - throw e; + await dispatch('getBranchData', { + projectId, + branchId: mr.source_branch, + }); + + const branch = getters.findBranch(projectId, mr.source_branch); + + await dispatch('getFiles', { + projectId, + branchId: mr.source_branch, + ref: branch.commit.id, }); + + await dispatch('getMergeRequestVersions', { + projectId, + targetProjectId, + mergeRequestId, + }); + + const { changes } = await dispatch('getMergeRequestChanges', { + projectId, + targetProjectId, + mergeRequestId, + }); + + await dispatch('openMergeRequestChanges', changes); + } catch (e) { + flash(__('Error while loading the merge request. Please try again.')); + throw e; + } +}; diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js index 651fc907611..8110934efc4 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js +++ b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js @@ -25,6 +25,14 @@ export function createResolvers({ endpoints }) { data: { availableNamespaces }, } = await client.query({ query: availableNamespacesQuery }); + if (!statusPoller) { + statusPoller = new StatusPoller({ + client, + pollPath: endpoints.jobs, + }); + statusPoller.startPolling(); + } + return axios .get(endpoints.status, { params: { @@ -83,7 +91,7 @@ export function createResolvers({ endpoints }) { const group = groupManager.findById(sourceGroupId); groupManager.setImportStatus(group, STATUSES.SCHEDULING); try { - await axios.post(endpoints.createBulkImport, { + const response = await axios.post(endpoints.createBulkImport, { bulk_import: [ { source_type: 'group_entity', @@ -94,10 +102,7 @@ export function createResolvers({ endpoints }) { ], }); groupManager.setImportStatus(group, STATUSES.STARTED); - if (!statusPoller) { - statusPoller = new StatusPoller({ client, interval: 3000 }); - statusPoller.startPolling(); - } + SourceGroupsManager.attachImportId(group, response.data.id); } catch (e) { createFlash({ message: s__('BulkImport|Importing the group failed'), diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js index 047b04fe7d6..261e30edbbb 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js +++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/source_groups_manager.js @@ -14,6 +14,12 @@ function generateGroupId(id) { } export class SourceGroupsManager { + static importMap = new Map(); + + static attachImportId(group, importId) { + SourceGroupsManager.importMap.set(importId, group.id); + } + constructor({ client }) { this.client = client; } @@ -36,6 +42,10 @@ export class SourceGroupsManager { this.update(group, fn); } + findByImportId(importId) { + return this.findById(SourceGroupsManager.importMap.get(importId)); + } + setImportStatus(group, status) { this.update(group, (sourceGroup) => { // eslint-disable-next-line no-param-reassign diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js index 960126cfa6d..63cd6b48fc4 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js +++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js @@ -1,71 +1,47 @@ -import gql from 'graphql-tag'; +import Visibility from 'visibilityjs'; import createFlash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; +import Poll from '~/lib/utils/poll'; import { s__ } from '~/locale'; -import { STATUSES } from '../../../constants'; -import bulkImportSourceGroupsQuery from '../queries/bulk_import_source_groups.query.graphql'; import { SourceGroupsManager } from './source_groups_manager'; -const groupId = (i) => `group${i}`; - -function generateGroupsQuery(groups) { - return gql`{ - ${groups - .map( - (g, idx) => - `${groupId(idx)}: group(fullPath: "${g.import_target.target_namespace}/${ - g.import_target.new_name - }") { id }`, - ) - .join('\n')} - }`; -} - export class StatusPoller { - constructor({ client, interval }) { + constructor({ client, pollPath }) { this.client = client; - this.interval = interval; - this.timeoutId = null; - this.groupManager = new SourceGroupsManager({ client }); - } - startPolling() { - if (this.timeoutId) { - return; - } + this.eTagPoll = new Poll({ + resource: { + fetchJobs: () => axios.get(pollPath), + }, + method: 'fetchJobs', + successCallback: ({ data }) => this.updateImportsStatuses(data), + errorCallback: () => + createFlash({ + message: s__('BulkImport|Update of import statuses with realtime changes failed'), + }), + }); + + Visibility.change(() => { + if (!Visibility.hidden()) { + this.eTagPoll.restart(); + } else { + this.eTagPoll.stop(); + } + }); - this.checkPendingImports(); + this.groupManager = new SourceGroupsManager({ client }); } - stopPolling() { - clearTimeout(this.timeoutId); - this.timeoutId = null; + startPolling() { + this.eTagPoll.makeRequest(); } - async checkPendingImports() { - try { - const { bulkImportSourceGroups } = this.client.readQuery({ - query: bulkImportSourceGroupsQuery, - }); - - const groupsInProgress = bulkImportSourceGroups.nodes.filter( - (g) => g.status === STATUSES.STARTED, - ); - if (groupsInProgress.length) { - const { data: results } = await this.client.query({ - query: generateGroupsQuery(groupsInProgress), - fetchPolicy: 'no-cache', - }); - const completedGroups = groupsInProgress.filter((_, idx) => Boolean(results[groupId(idx)])); - completedGroups.forEach((group) => { - this.groupManager.setImportStatus(group, STATUSES.FINISHED); - }); + async updateImportsStatuses(importStatuses) { + importStatuses.forEach(({ id, status_name: statusName }) => { + const group = this.groupManager.findByImportId(id); + if (group.id) { + this.groupManager.setImportStatus(group, statusName); } - } catch (e) { - createFlash({ - message: s__('BulkImport|Update of import statuses with realtime changes failed'), - }); - } finally { - this.timeoutId = setTimeout(() => this.checkPendingImports(), this.interval); - } + }); } } diff --git a/app/assets/javascripts/import_entities/import_groups/index.js b/app/assets/javascripts/import_entities/import_groups/index.js index cd646befaaa..cd837a840e4 100644 --- a/app/assets/javascripts/import_entities/import_groups/index.js +++ b/app/assets/javascripts/import_entities/import_groups/index.js @@ -14,6 +14,7 @@ export function mountImportGroupsApp(mountElement) { statusPath, availableNamespacesPath, createBulkImportPath, + jobsPath, sourceUrl, } = mountElement.dataset; const apolloProvider = new VueApollo({ @@ -22,6 +23,7 @@ export function mountImportGroupsApp(mountElement) { status: statusPath, availableNamespaces: availableNamespacesPath, createBulkImport: createBulkImportPath, + jobs: jobsPath, }, }), }); diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index 7107380b146..91ab68d5f39 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -4,7 +4,6 @@ import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import { throttle, isEmpty } from 'lodash'; import { mapGetters, mapState, mapActions } from 'vuex'; import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; -import { polyfillSticky } from '~/lib/utils/sticky'; import { sprintf } from '~/locale'; import CiHeader from '~/vue_shared/components/header_ci_component.vue'; import delayedJobMixin from '../mixins/delayed_job_mixin'; @@ -135,14 +134,6 @@ export default { this.fetchJobsForStage(defaultStage); } } - - if (newVal.archived) { - this.$nextTick(() => { - if (this.$refs.sticky) { - polyfillSticky(this.$refs.sticky); - } - }); - } }, }, created() { @@ -265,7 +256,6 @@ export default { <div v-if="job.archived" - ref="sticky" class="gl-mt-3 archived-job" :class="{ 'sticky-top border-bottom-0': hasTrace }" data-testid="archived-job" diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue index 2453fea9e58..fbdbfddff56 100644 --- a/app/assets/javascripts/jobs/components/job_log_controllers.vue +++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue @@ -2,7 +2,6 @@ /* eslint-disable vue/no-v-html */ import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui'; import { numberToHumanSize } from '~/lib/utils/number_utils'; -import { polyfillSticky } from '~/lib/utils/sticky'; import { __, sprintf } from '~/locale'; import scrollDown from '../svg/scroll_down.svg'; @@ -54,9 +53,6 @@ export default { }); }, }, - mounted() { - polyfillSticky(this.$el); - }, methods: { handleScrollToTop() { this.$emit('scrollJobLogTop'); diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js index 6bb7f09b886..a6d53358cb8 100644 --- a/app/assets/javascripts/lib/utils/sticky.js +++ b/app/assets/javascripts/lib/utils/sticky.js @@ -1,5 +1,3 @@ -import StickyFill from 'stickyfilljs'; - export const createPlaceholder = () => { const placeholder = document.createElement('div'); placeholder.classList.add('sticky-placeholder'); @@ -60,13 +58,3 @@ export const stickyMonitor = (el, stickyTop, insertPlaceholder = true) => { }, ); }; - -/** - * Polyfill the `position: sticky` behavior. - * - * - If the current environment supports `position: sticky`, do nothing. - * - Can receive an iterable element list (NodeList, jQuery collection, etc.) or single HTMLElement. - */ -export const polyfillSticky = (el) => { - StickyFill.add(el); -}; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 6aa45ecc7a0..251f1e0515a 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -19,7 +19,6 @@ import { } from './lib/utils/common_utils'; import { localTimeAgo } from './lib/utils/datetime_utility'; import { isInVueNoteablePage } from './lib/utils/dom_utils'; -import { polyfillSticky } from './lib/utils/sticky'; import { getLocationHash } from './lib/utils/url_utility'; import { __ } from './locale'; import Notes from './notes'; @@ -123,7 +122,6 @@ export default class MergeRequestTabs { ) { this.mergeRequestTabs.querySelector(`a[data-action='${action}']`).click(); } - this.initAffix(); } bindEvents() { @@ -509,21 +507,4 @@ export default class MergeRequestTabs { } }, 0); } - - initAffix() { - const $tabs = $('.js-tabs-affix'); - - // Screen space on small screens is usually very sparse - // So we dont affix the tabs on these - if (bp.getBreakpointSize() === 'xs' || !$tabs.length) return; - - /** - If the browser does not support position sticky, it returns the position as static. - If the browser does support sticky, then we allow the browser to handle it, if not - then we default back to Bootstraps affix - */ - if ($tabs.css('position') !== 'static') return; - - polyfillSticky($tabs); - } } diff --git a/app/assets/javascripts/pages/import/bulk_imports/index.js b/app/assets/javascripts/pages/import/bulk_imports/status/index.js index 37ac1a98466..37ac1a98466 100644 --- a/app/assets/javascripts/pages/import/bulk_imports/index.js +++ b/app/assets/javascripts/pages/import/bulk_imports/status/index.js diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 1383f224979..f56d8f2c2a9 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -497,7 +497,7 @@ li { a, button, - .dropdown-item { + .dropdown-item:not(.open-with-link) { padding: 8px 40px; position: relative; diff --git a/app/controllers/import/bulk_imports_controller.rb b/app/controllers/import/bulk_imports_controller.rb index 61eb9a27560..ef32ba4d119 100644 --- a/app/controllers/import/bulk_imports_controller.rb +++ b/app/controllers/import/bulk_imports_controller.rb @@ -37,9 +37,8 @@ class Import::BulkImportsController < ApplicationController end def create - BulkImportService.new(current_user, create_params, credentials).execute - - render json: :ok + result = BulkImportService.new(current_user, create_params, credentials).execute + render json: result.to_json(only: [:id]) end def realtime_changes diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index c30fc0f5a73..c9e9a34ad88 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -36,7 +36,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo push_frontend_feature_flag(:merge_request_widget_graphql, @project) push_frontend_feature_flag(:drag_comment_selection, @project, default_enabled: true) push_frontend_feature_flag(:unified_diff_components, @project, default_enabled: true) - push_frontend_feature_flag(:default_merge_ref_for_diffs, @project) + push_frontend_feature_flag(:default_merge_ref_for_diffs, @project, default_enabled: :yaml) push_frontend_feature_flag(:core_security_mr_widget_counts, @project) push_frontend_feature_flag(:remove_resolve_note, @project, default_enabled: true) push_frontend_feature_flag(:diffs_gradual_load, @project, default_enabled: true) @@ -502,7 +502,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo params = request.query_parameters params[:view] = "inline" - if Feature.enabled?(:default_merge_ref_for_diffs, project) + if Feature.enabled?(:default_merge_ref_for_diffs, project, default_enabled: :yaml) params = params.merge(diff_head: true) end diff --git a/app/controllers/repositories/git_http_controller.rb b/app/controllers/repositories/git_http_controller.rb index 3cf0a23b7f6..9ad700404ff 100644 --- a/app/controllers/repositories/git_http_controller.rb +++ b/app/controllers/repositories/git_http_controller.rb @@ -78,6 +78,8 @@ module Repositories def update_fetch_statistics return unless project return if Gitlab::Database.read_only? + return if Feature.enabled?(:disable_git_http_fetch_writes) + return unless repo_type.project? OnboardingProgressService.new(project.namespace).execute(action: :git_read) diff --git a/app/finders/ci/jobs_finder.rb b/app/finders/ci/jobs_finder.rb index 4ade3e6f031..4408c9cdb6d 100644 --- a/app/finders/ci/jobs_finder.rb +++ b/app/finders/ci/jobs_finder.rb @@ -45,7 +45,8 @@ module Ci return unless pipeline raise Gitlab::Access::AccessDeniedError unless can?(current_user, :read_build, pipeline) - jobs_by_type(pipeline, type).latest + jobs_scope = jobs_by_type(pipeline, type) + params[:include_retried] ? jobs_scope : jobs_scope.latest end def filter_by_scope(builds) diff --git a/app/finders/deployments_finder.rb b/app/finders/deployments_finder.rb index bdcf7da3bea..89a28d9dfb8 100644 --- a/app/finders/deployments_finder.rb +++ b/app/finders/deployments_finder.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# WARNING: This finder does not check permissions! +# # Arguments: # params: # project: Project model - Find deployments for this project @@ -27,11 +29,13 @@ class DeploymentsFinder def execute items = init_collection items = by_updated_at(items) + items = by_finished_at(items) items = by_environment(items) items = by_status(items) items = preload_associations(items) - items = by_finished_between(items) - sort(items) + items = sort(items) + + items end private @@ -44,11 +48,9 @@ class DeploymentsFinder end end - # rubocop: disable CodeReuse/ActiveRecord def sort(items) - items.order(sort_params) + items.order(sort_params) # rubocop: disable CodeReuse/ActiveRecord end - # rubocop: enable CodeReuse/ActiveRecord def by_updated_at(items) items = items.updated_before(params[:updated_before]) if params[:updated_before].present? @@ -57,6 +59,13 @@ class DeploymentsFinder items end + def by_finished_at(items) + items = items.finished_before(params[:finished_before]) if params[:finished_before].present? + items = items.finished_after(params[:finished_after]) if params[:finished_after].present? + + items + end + def by_environment(items) if params[:environment].present? items.for_environment_name(params[:environment]) @@ -65,12 +74,6 @@ class DeploymentsFinder end end - def by_finished_between(items) - items = items.finished_between(params[:finished_after], params[:finished_before].presence) if params[:finished_after].present? - - items - end - def by_status(items) return items unless params[:status].present? diff --git a/app/graphql/mutations/merge_requests/update.rb b/app/graphql/mutations/merge_requests/update.rb index 4721ebab41b..6a94d2f37b2 100644 --- a/app/graphql/mutations/merge_requests/update.rb +++ b/app/graphql/mutations/merge_requests/update.rb @@ -19,9 +19,14 @@ module Mutations required: false, description: copy_field_description(Types::MergeRequestType, :description) - def resolve(args) - merge_request = authorized_find!(**args.slice(:project_path, :iid)) - attributes = args.slice(:title, :description, :target_branch).compact + argument :state, ::Types::MergeRequestStateEventEnum, + required: false, + as: :state_event, + description: 'The action to perform to change the state.' + + def resolve(project_path:, iid:, **args) + merge_request = authorized_find!(project_path: project_path, iid: iid) + attributes = args.compact ::MergeRequests::UpdateService .new(merge_request.project, current_user, attributes) diff --git a/app/graphql/types/merge_request_state_event_enum.rb b/app/graphql/types/merge_request_state_event_enum.rb new file mode 100644 index 00000000000..ebb8b9638db --- /dev/null +++ b/app/graphql/types/merge_request_state_event_enum.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + class MergeRequestStateEventEnum < BaseEnum + graphql_name 'MergeRequestNewState' + description 'New state to apply to a merge request.' + + value 'OPEN', + value: 'reopen', + description: 'Open the merge request if it is closed.' + + value 'CLOSED', + value: 'close', + description: 'Close the merge request if it is open.' + end +end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 312d535a92c..cfc4075100b 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -80,27 +80,27 @@ module LabelsHelper def suggested_colors { - '#0033CC' => s_('SuggestedColors|UA blue'), - '#428BCA' => s_('SuggestedColors|Moderate blue'), - '#44AD8E' => s_('SuggestedColors|Lime green'), - '#A8D695' => s_('SuggestedColors|Feijoa'), - '#5CB85C' => s_('SuggestedColors|Slightly desaturated green'), - '#69D100' => s_('SuggestedColors|Bright green'), - '#004E00' => s_('SuggestedColors|Very dark lime green'), - '#34495E' => s_('SuggestedColors|Very dark desaturated blue'), - '#7F8C8D' => s_('SuggestedColors|Dark grayish cyan'), - '#A295D6' => s_('SuggestedColors|Slightly desaturated blue'), - '#5843AD' => s_('SuggestedColors|Dark moderate blue'), - '#8E44AD' => s_('SuggestedColors|Dark moderate violet'), - '#FFECDB' => s_('SuggestedColors|Very pale orange'), - '#AD4363' => s_('SuggestedColors|Dark moderate pink'), - '#D10069' => s_('SuggestedColors|Strong pink'), - '#CC0033' => s_('SuggestedColors|Strong red'), - '#FF0000' => s_('SuggestedColors|Pure red'), - '#D9534F' => s_('SuggestedColors|Soft red'), - '#D1D100' => s_('SuggestedColors|Strong yellow'), - '#F0AD4E' => s_('SuggestedColors|Soft orange'), - '#AD8D43' => s_('SuggestedColors|Dark moderate orange') + '#009966' => s_('SuggestedColors|Green-cyan'), + '#8fbc8f' => s_('SuggestedColors|Dark sea green'), + '#3cb371' => s_('SuggestedColors|Medium sea green'), + '#00b140' => s_('SuggestedColors|Green screen'), + '#013220' => s_('SuggestedColors|Dark green'), + '#6699cc' => s_('SuggestedColors|Blue-gray'), + '#0000ff' => s_('SuggestedColors|Blue'), + '#e6e6fa' => s_('SuggestedColors|Lavendar'), + '#9400d3' => s_('SuggestedColors|Dark violet'), + '#330066' => s_('SuggestedColors|Deep violet'), + '#808080' => s_('SuggestedColors|Gray'), + '#36454f' => s_('SuggestedColors|Charcoal grey'), + '#f7e7ce' => s_('SuggestedColors|Champagne'), + '#c21e56' => s_('SuggestedColors|Rose red'), + '#cc338b' => s_('SuggestedColors|Magenta-pink'), + '#dc143c' => s_('SuggestedColors|Crimson'), + '#ff0000' => s_('SuggestedColors|Red'), + '#cd5b45' => s_('SuggestedColors|Dark coral'), + '#eee600' => s_('SuggestedColors|Titanium yellow'), + '#ed9121' => s_('SuggestedColors|Carrot orange'), + '#c39953' => s_('SuggestedColors|Aztec Gold') } end diff --git a/app/models/clusters/agent_token.rb b/app/models/clusters/agent_token.rb index 5c9561ffa98..b260822f784 100644 --- a/app/models/clusters/agent_token.rb +++ b/app/models/clusters/agent_token.rb @@ -8,6 +8,7 @@ module Clusters self.table_name = 'cluster_agent_tokens' belongs_to :agent, class_name: 'Clusters::Agent' + belongs_to :created_by_user, class_name: 'User', optional: true before_save :ensure_token end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 2f0fd0af63b..ea2f425c5f6 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -209,14 +209,26 @@ class CommitStatus < ApplicationRecord end def group_name - # 'rspec:linux: 1/10' => 'rspec:linux' - common_name = name.to_s.gsub(%r{\b\d+[\s:\/\\]+\d+\s*}, '') + simplified_commit_status_group_name_feature_flag = Gitlab::SafeRequestStore.fetch("project:#{project_id}:simplified_commit_status_group_name") do + Feature.enabled?(:simplified_commit_status_group_name, project, default_enabled: false) + end + + if simplified_commit_status_group_name_feature_flag + # Only remove one or more [...] "X/Y" "X Y" from the end of build names. + # More about the regular expression logic: https://docs.gitlab.com/ee/ci/jobs/#group-jobs-in-a-pipeline - # 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux' - common_name.gsub!(%r{: \[.*\]\s*\z}, '') + name.to_s.sub(%r{([\b\s:]+((\[.*\])|(\d+[\s:\/\\]+\d+)))+\s*\z}, '').strip + else + # Prior implementation, remove [...] "X/Y" "X Y" from the beginning and middle of build names + # 'rspec:linux: 1/10' => 'rspec:linux' + common_name = name.to_s.gsub(%r{\b\d+[\s:\/\\]+\d+\s*}, '') - common_name.strip! - common_name + # 'rspec:linux: [aws, max memory]' => 'rspec:linux', 'rspec:linux: [aws]' => 'rspec:linux' + common_name.gsub!(%r{: \[.*\]\s*\z}, '') + + common_name.strip! + common_name + end end def failed_but_allowed? diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 7bcf7c702f6..f000e474605 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -38,6 +38,7 @@ class Deployment < ApplicationRecord scope :for_status, -> (status) { where(status: status) } scope :for_project, -> (project_id) { where(project_id: project_id) } + scope :for_projects, -> (projects) { where(project: projects) } scope :visible, -> { where(status: %i[running success failed canceled]) } scope :stoppable, -> { where.not(on_stop: nil).where.not(deployable_id: nil).success } @@ -45,11 +46,8 @@ class Deployment < ApplicationRecord scope :older_than, -> (deployment) { where('deployments.id < ?', deployment.id) } scope :with_deployable, -> { joins('INNER JOIN ci_builds ON ci_builds.id = deployments.deployable_id').preload(:deployable) } - scope :finished_between, -> (start_date, end_date = nil) do - selected = where('deployments.finished_at >= ?', start_date) - selected = selected.where('deployments.finished_at < ?', end_date) if end_date - selected - end + scope :finished_after, ->(date) { where('finished_at >= ?', date) } + scope :finished_before, ->(date) { where('finished_at < ?', date) } FINISHED_STATUSES = %i[success failed canceled].freeze diff --git a/app/models/label.rb b/app/models/label.rb index 54129c7c7f3..7a31b095cfc 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -12,7 +12,7 @@ class Label < ApplicationRecord cache_markdown_field :description, pipeline: :single_line - DEFAULT_COLOR = '#428BCA' + DEFAULT_COLOR = '#6699cc' default_value_for :color, DEFAULT_COLOR diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 5fad876d3fb..1374e8a814a 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -921,6 +921,10 @@ class MergeRequest < ApplicationRecord closed? && !source_project_missing? && source_branch_exists? end + def can_be_closed? + opened? + end + def ensure_merge_request_diff merge_request_diff.persisted? || create_merge_request_diff end diff --git a/app/services/bulk_import_service.rb b/app/services/bulk_import_service.rb index bebf9153ce7..29439a79afe 100644 --- a/app/services/bulk_import_service.rb +++ b/app/services/bulk_import_service.rb @@ -38,6 +38,8 @@ class BulkImportService bulk_import = create_bulk_import BulkImportWorker.perform_async(bulk_import.id) + + bulk_import end private diff --git a/app/services/merge_requests/reload_merge_head_diff_service.rb b/app/services/merge_requests/reload_merge_head_diff_service.rb index 66fcb5c022b..f02a9bd3139 100644 --- a/app/services/merge_requests/reload_merge_head_diff_service.rb +++ b/app/services/merge_requests/reload_merge_head_diff_service.rb @@ -24,7 +24,7 @@ module MergeRequests attr_reader :merge_request def enabled? - Feature.enabled?(:default_merge_ref_for_diffs, merge_request.project) + Feature.enabled?(:default_merge_ref_for_diffs, merge_request.project, default_enabled: :yaml) end def recreate_merge_head_diff diff --git a/app/views/import/bulk_imports/status.html.haml b/app/views/import/bulk_imports/status.html.haml index 9ae71eabc8e..778bc1ef1a4 100644 --- a/app/views/import/bulk_imports/status.html.haml +++ b/app/views/import/bulk_imports/status.html.haml @@ -8,4 +8,5 @@ #import-groups-mount-element{ data: { status_path: status_import_bulk_imports_path(format: :json), available_namespaces_path: import_available_namespaces_path(format: :json), create_bulk_import_path: import_bulk_imports_path(format: :json), + jobs_path: realtime_changes_import_bulk_imports_path(format: :json), source_url: @source_url } } diff --git a/app/views/projects/buttons/_clone.html.haml b/app/views/projects/buttons/_clone.html.haml index 938dfc69500..0ec47744fc9 100644 --- a/app/views/projects/buttons/_clone.html.haml +++ b/app/views/projects/buttons/_clone.html.haml @@ -6,9 +6,9 @@ %span.gl-mr-2.js-clone-dropdown-label = _('Clone') = sprite_icon("chevron-down", css_class: "icon") - %ul.p-3.dropdown-menu.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown.qa-clone-options{ class: dropdown_class } + %ul.dropdown-menu.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown.qa-clone-options{ class: dropdown_class } - if ssh_enabled? - %li + %li{ class: 'gl-px-4!' } %label.label-bold = _('Clone with SSH') .input-group @@ -17,7 +17,7 @@ = clipboard_button(target: '#ssh_project_clone', title: _("Copy URL"), class: "input-group-text btn-default btn-clipboard") = render_if_exists 'projects/buttons/geo' - if http_enabled? - %li.pt-2 + %li.pt-2{ class: 'gl-px-4!' } %label.label-bold = _('Clone with %{http_label}') % { http_label: gitlab_config.protocol.upcase } .input-group @@ -25,4 +25,15 @@ .input-group-append = clipboard_button(target: '#http_project_clone', title: _("Copy URL"), class: "input-group-text btn-default btn-clipboard") = render_if_exists 'projects/buttons/geo' + %li.divider.mt-2 + %li.pt-2.gl-new-dropdown-item + %label.label-bold{ class: 'gl-px-4!' } + = _('Open in your IDE') + %a.dropdown-item.open-with-link{ href: 'vscode://vscode.git/clone?url=' + CGI.escape(project.http_url_to_repo) } + .gl-new-dropdown-item-text-wrapper + = _('Visual Studio Code') + - if show_xcode_link?(@project) + %a.dropdown-item.open-with-link{ href: xcode_uri_to_repo(@project) } + .gl-new-dropdown-item-text-wrapper + = _("Xcode") = render_if_exists 'projects/buttons/kerberos_clone_field' diff --git a/app/views/projects/buttons/_xcode_link.html.haml b/app/views/projects/buttons/_xcode_link.html.haml deleted file mode 100644 index e0f47f1ca3d..00000000000 --- a/app/views/projects/buttons/_xcode_link.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -%a.gl-button.btn.btn-default{ href: xcode_uri_to_repo(@project) } - = _("Open in Xcode") diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 2a502f6e613..453a34d1e7a 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -78,7 +78,7 @@ - if mr_action === "diffs" - add_page_startup_api_call @endpoint_metadata_url - params = request.query_parameters - - if Feature.enabled?(:default_merge_ref_for_diffs, @project) + - if Feature.enabled?(:default_merge_ref_for_diffs, @project, default_enabled: :yaml) - params = params.merge(diff_head: true) = render "projects/merge_requests/tabs/pane", name: "diffs", id: "js-diffs-app", class: "diffs", data: { "is-locked": @merge_request.discussion_locked?, endpoint: diffs_project_merge_request_path(@project, @merge_request, 'json', params), diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index cd6e85d60ed..6d33fbb535e 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -11,11 +11,6 @@ = render 'projects/find_file_link' = render 'shared/web_ide_button', blob: nil - - - if show_xcode_link?(@project) - .project-action-button.project-xcode< - = render "projects/buttons/xcode_link" - = render 'projects/buttons/download', project: @project, ref: @ref .project-clone-holder.d-none.d-md-inline-block> diff --git a/app/views/shared/notifications/_new_button.html.haml b/app/views/shared/notifications/_new_button.html.haml index 14f4b04ef78..4b008601783 100644 --- a/app/views/shared/notifications/_new_button.html.haml +++ b/app/views/shared/notifications/_new_button.html.haml @@ -17,14 +17,14 @@ .js-notification-toggle-btns %div{ class: ("btn-group" if notification_setting.custom?) } - if notification_setting.custom? - %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => button_title, data: { container: "body", placement: 'top', toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } } + %button.dropdown-new.btn.gl-button.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => button_title, data: { container: "body", placement: 'top', toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } } = notification_setting_icon(notification_setting) %span.js-notification-loading.fa.hidden - %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" }, class: "#{btn_class}" } + %button.btn.gl-button.btn-default.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" }, class: "#{btn_class}" } = sprite_icon("chevron-down", css_class: "icon mr-0") .sr-only Toggle dropdown - else - %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => button_title, data: { container: "body", placement: 'top', toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } + %button.dropdown-new.btn.gl-button.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => button_title, data: { container: "body", placement: 'top', toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } = notification_setting_icon(notification_setting) %span.js-notification-loading.fa.hidden = sprite_icon("chevron-down", css_class: "icon") diff --git a/app/workers/pages_transfer_worker.rb b/app/workers/pages_transfer_worker.rb index f78564cc69d..5d395c9e38a 100644 --- a/app/workers/pages_transfer_worker.rb +++ b/app/workers/pages_transfer_worker.rb @@ -9,7 +9,7 @@ class PagesTransferWorker # rubocop:disable Scalability/IdempotentWorker loggable_arguments 0, 1 def perform(method, args) - return unless Gitlab::PagesTransfer::Async::METHODS.include?(method) + return unless Gitlab::PagesTransfer::METHODS.include?(method) result = Gitlab::PagesTransfer.new.public_send(method, *args) # rubocop:disable GitlabSecurity/PublicSend |