diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-21 18:07:57 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-21 18:07:57 +0000 |
commit | c0b718a0dbd99e6c0d30e5bc55bdcf4a12946375 (patch) | |
tree | 8ad3691912d91d8cf7b3931f68a4284ae7b5995c /app | |
parent | 5dc70663c4ff1feb215428ce50673b5b646f9809 (diff) | |
download | gitlab-ce-c0b718a0dbd99e6c0d30e5bc55bdcf4a12946375.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
23 files changed, 244 insertions, 73 deletions
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/render.js b/app/assets/javascripts/deprecated_jquery_dropdown/render.js index 0f612989bb4..97698d55011 100644 --- a/app/assets/javascripts/deprecated_jquery_dropdown/render.js +++ b/app/assets/javascripts/deprecated_jquery_dropdown/render.js @@ -149,7 +149,7 @@ function renderLink(row, data, { options, group, index }) { } function getOptionRenderer({ options, instance }) { - return options.renderRow && ((li, data) => options.renderRow(data, instance)); + return options.renderRow && ((li, data, params) => options.renderRow(data, instance, params)); } function getRenderer(data, params) { diff --git a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js index d9c627f5c93..397ba879866 100644 --- a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js +++ b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js @@ -1,9 +1,16 @@ -import { __, s__ } from '~/locale'; +import { __ } from '~/locale'; +import { + TOKEN_TITLE_APPROVED_BY, + TOKEN_TITLE_REVIEWER, + TOKEN_TYPE_APPROVED_BY, + TOKEN_TYPE_REVIEWER, + TOKEN_TYPE_TARGET_BRANCH, +} from '~/vue_shared/components/filtered_search_bar/constants'; export default (IssuableTokenKeys, disableTargetBranchFilter = false) => { const reviewerToken = { - formattedKey: s__('SearchToken|Reviewer'), - key: 'reviewer', + formattedKey: TOKEN_TITLE_REVIEWER, + key: TOKEN_TYPE_REVIEWER, type: 'string', param: 'username', symbol: '@', @@ -53,7 +60,7 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => { if (!disableTargetBranchFilter) { const targetBranchToken = { formattedKey: __('Target-Branch'), - key: 'target-branch', + key: TOKEN_TYPE_TARGET_BRANCH, type: 'string', param: '', symbol: '', @@ -67,8 +74,8 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => { const approvedBy = { token: { - formattedKey: __('Approved-By'), - key: 'approved-by', + formattedKey: TOKEN_TITLE_APPROVED_BY, + key: TOKEN_TYPE_APPROVED_BY, type: 'array', param: 'usernames[]', symbol: '@', @@ -76,8 +83,8 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => { tag: '@approved-by', }, tokenAlternative: { - formattedKey: __('Approved-By'), - key: 'approved-by', + formattedKey: TOKEN_TITLE_APPROVED_BY, + key: TOKEN_TYPE_APPROVED_BY, type: 'string', param: 'usernames', symbol: '@', @@ -85,25 +92,25 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => { condition: [ { url: 'approved_by_usernames[]=None', - tokenKey: 'approved-by', + tokenKey: TOKEN_TYPE_APPROVED_BY, value: __('None'), operator: '=', }, { url: 'not[approved_by_usernames][]=None', - tokenKey: 'approved-by', + tokenKey: TOKEN_TYPE_APPROVED_BY, value: __('None'), operator: '!=', }, { url: 'approved_by_usernames[]=Any', - tokenKey: 'approved-by', + tokenKey: TOKEN_TYPE_APPROVED_BY, value: __('Any'), operator: '=', }, { url: 'not[approved_by_usernames][]=Any', - tokenKey: 'approved-by', + tokenKey: TOKEN_TYPE_APPROVED_BY, value: __('Any'), operator: '!=', }, diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js index 3913e4e8d81..1f8baa470d8 100644 --- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js +++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js @@ -1,5 +1,17 @@ import { sortMilestonesByDueDate } from '~/milestones/utils'; -import { mergeUrlParams } from '../lib/utils/url_utility'; +import { mergeUrlParams } from '~/lib/utils/url_utility'; +import { + TOKEN_TYPE_APPROVED_BY, + TOKEN_TYPE_ASSIGNEE, + TOKEN_TYPE_AUTHOR, + TOKEN_TYPE_CONFIDENTIAL, + TOKEN_TYPE_LABEL, + TOKEN_TYPE_MILESTONE, + TOKEN_TYPE_MY_REACTION, + TOKEN_TYPE_RELEASE, + TOKEN_TYPE_REVIEWER, + TOKEN_TYPE_TARGET_BRANCH, +} from '~/vue_shared/components/filtered_search_bar/constants'; import DropdownEmoji from './dropdown_emoji'; import DropdownHint from './dropdown_hint'; import DropdownNonUser from './dropdown_non_user'; @@ -58,17 +70,17 @@ export default class AvailableDropdownMappings { getMappings() { return { - author: { + [TOKEN_TYPE_AUTHOR]: { reference: null, gl: DropdownUser, element: this.container.querySelector('#js-dropdown-author'), }, - assignee: { + [TOKEN_TYPE_ASSIGNEE]: { reference: null, gl: DropdownUser, element: this.container.querySelector('#js-dropdown-assignee'), }, - reviewer: { + [TOKEN_TYPE_REVIEWER]: { reference: null, gl: DropdownUser, element: this.container.querySelector('#js-dropdown-reviewer'), @@ -78,12 +90,12 @@ export default class AvailableDropdownMappings { gl: DropdownUser, element: this.container.getElementById('js-dropdown-attention-requested'), }, - 'approved-by': { + [TOKEN_TYPE_APPROVED_BY]: { reference: null, gl: DropdownUser, element: this.container.querySelector('#js-dropdown-approved-by'), }, - milestone: { + [TOKEN_TYPE_MILESTONE]: { reference: null, gl: DropdownNonUser, extraArguments: { @@ -93,7 +105,7 @@ export default class AvailableDropdownMappings { }, element: this.container.querySelector('#js-dropdown-milestone'), }, - release: { + [TOKEN_TYPE_RELEASE]: { reference: null, gl: DropdownNonUser, extraArguments: { @@ -106,7 +118,7 @@ export default class AvailableDropdownMappings { }, element: this.container.querySelector('#js-dropdown-release'), }, - label: { + [TOKEN_TYPE_LABEL]: { reference: null, gl: DropdownNonUser, extraArguments: { @@ -116,7 +128,7 @@ export default class AvailableDropdownMappings { }, element: this.container.querySelector('#js-dropdown-label'), }, - 'my-reaction': { + [TOKEN_TYPE_MY_REACTION]: { reference: null, gl: DropdownEmoji, element: this.container.querySelector('#js-dropdown-my-reaction'), @@ -126,12 +138,12 @@ export default class AvailableDropdownMappings { gl: DropdownNonUser, element: this.container.querySelector('#js-dropdown-wip'), }, - confidential: { + [TOKEN_TYPE_CONFIDENTIAL]: { reference: null, gl: DropdownNonUser, element: this.container.querySelector('#js-dropdown-confidential'), }, - 'target-branch': { + [TOKEN_TYPE_TARGET_BRANCH]: { reference: null, gl: DropdownNonUser, extraArguments: { diff --git a/app/assets/javascripts/filtered_search/constants.js b/app/assets/javascripts/filtered_search/constants.js index e07dccd11e8..b328ae6a872 100644 --- a/app/assets/javascripts/filtered_search/constants.js +++ b/app/assets/javascripts/filtered_search/constants.js @@ -1,4 +1,17 @@ -export const USER_TOKEN_TYPES = ['author', 'assignee', 'approved-by', 'reviewer', 'attention']; +import { + TOKEN_TYPE_APPROVED_BY, + TOKEN_TYPE_ASSIGNEE, + TOKEN_TYPE_AUTHOR, + TOKEN_TYPE_REVIEWER, +} from '~/vue_shared/components/filtered_search_bar/constants'; + +export const USER_TOKEN_TYPES = [ + TOKEN_TYPE_AUTHOR, + TOKEN_TYPE_ASSIGNEE, + TOKEN_TYPE_APPROVED_BY, + TOKEN_TYPE_REVIEWER, + 'attention', +]; export const DROPDOWN_TYPE = { hint: 'hint', diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index 22e1604871a..38909db0555 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -1,4 +1,5 @@ import { last } from 'lodash'; +import { TOKEN_TYPE_LABEL } from '~/vue_shared/components/filtered_search_bar/constants'; import FilteredSearchContainer from './container'; import FilteredSearchDropdownManager from './filtered_search_dropdown_manager'; import FilteredSearchTokenizer from './filtered_search_tokenizer'; @@ -113,7 +114,7 @@ export default class DropdownUtils { visualToken && visualToken.querySelector('.value') && visualToken.querySelector('.value').textContent.trim(); - if (tokenName === 'label' && tokenValue) { + if (tokenName === TOKEN_TYPE_LABEL && tokenValue) { // remove leading symbol and wrapping quotes tokenValue = tokenValue.replace(/^~("|')?(.*)/, '$2').replace(/("|')$/, ''); } diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index bc0f5398b4c..16c70fdd069 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -10,8 +10,12 @@ import { DOWN_KEY_CODE, } from '~/lib/utils/keycodes'; import { __ } from '~/locale'; -import { addClassIfElementExists } from '../lib/utils/dom_utils'; -import { visitUrl, getUrlParamsArray, getParameterByName } from '../lib/utils/url_utility'; +import { addClassIfElementExists } from '~/lib/utils/dom_utils'; +import { visitUrl, getUrlParamsArray, getParameterByName } from '~/lib/utils/url_utility'; +import { + TOKEN_TYPE_ASSIGNEE, + TOKEN_TYPE_AUTHOR, +} from '~/vue_shared/components/filtered_search_bar/constants'; import FilteredSearchContainer from './container'; import DropdownUtils from './dropdown_utils'; import eventHub from './event_hub'; @@ -675,7 +679,7 @@ export default class FilteredSearchManager { const id = parseInt(value, 10); if (usernameParams[id]) { hasFilteredSearch = true; - const tokenName = 'assignee'; + const tokenName = TOKEN_TYPE_ASSIGNEE; const canEdit = this.canEdit && this.canEdit(tokenName); const operator = FilteredSearchVisualTokens.getOperatorToken(usernameParams[id]); const valueToken = FilteredSearchVisualTokens.getValueToken(usernameParams[id]); @@ -688,7 +692,7 @@ export default class FilteredSearchManager { const id = parseInt(value, 10); if (usernameParams[id]) { hasFilteredSearch = true; - const tokenName = 'author'; + const tokenName = TOKEN_TYPE_AUTHOR; const canEdit = this.canEdit && this.canEdit(tokenName); const operator = FilteredSearchVisualTokens.getOperatorToken(usernameParams[id]); const valueToken = FilteredSearchVisualTokens.getValueToken(usernameParams[id]); diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js index d6e7887f93f..8aa99ec52f9 100644 --- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js @@ -7,13 +7,20 @@ import { TOKEN_TITLE_MILESTONE, TOKEN_TITLE_MY_REACTION, TOKEN_TITLE_RELEASE, + TOKEN_TYPE_ASSIGNEE, + TOKEN_TYPE_AUTHOR, + TOKEN_TYPE_LABEL, + TOKEN_TYPE_MILESTONE, + TOKEN_TYPE_MY_REACTION, + TOKEN_TYPE_RELEASE, + TOKEN_TYPE_REVIEWER, } from '~/vue_shared/components/filtered_search_bar/constants'; import FilteredSearchTokenKeys from './filtered_search_token_keys'; export const tokenKeys = [ { formattedKey: TOKEN_TITLE_AUTHOR, - key: 'author', + key: TOKEN_TYPE_AUTHOR, type: 'string', param: 'username', symbol: '@', @@ -22,7 +29,7 @@ export const tokenKeys = [ }, { formattedKey: TOKEN_TITLE_ASSIGNEE, - key: 'assignee', + key: TOKEN_TYPE_ASSIGNEE, type: 'string', param: 'username', symbol: '@', @@ -31,7 +38,7 @@ export const tokenKeys = [ }, { formattedKey: TOKEN_TITLE_MILESTONE, - key: 'milestone', + key: TOKEN_TYPE_MILESTONE, type: 'string', param: 'title', symbol: '%', @@ -40,7 +47,7 @@ export const tokenKeys = [ }, { formattedKey: TOKEN_TITLE_RELEASE, - key: 'release', + key: TOKEN_TYPE_RELEASE, type: 'string', param: 'tag', symbol: '', @@ -49,7 +56,7 @@ export const tokenKeys = [ }, { formattedKey: TOKEN_TITLE_LABEL, - key: 'label', + key: TOKEN_TYPE_LABEL, type: 'array', param: 'name[]', symbol: '~', @@ -62,7 +69,7 @@ if (gon.current_user_id) { // Appending tokenkeys only logged-in tokenKeys.push({ formattedKey: TOKEN_TITLE_MY_REACTION, - key: 'my-reaction', + key: TOKEN_TYPE_MY_REACTION, type: 'string', param: 'emoji', symbol: '', @@ -74,7 +81,7 @@ if (gon.current_user_id) { export const alternativeTokenKeys = [ { formattedKey: TOKEN_TITLE_LABEL, - key: 'label', + key: TOKEN_TYPE_LABEL, type: 'string', param: 'name', symbol: '~', @@ -85,77 +92,77 @@ export const conditions = flattenDeep( [ { url: 'assignee_id=None', - tokenKey: 'assignee', + tokenKey: TOKEN_TYPE_ASSIGNEE, value: __('None'), }, { url: 'assignee_id=Any', - tokenKey: 'assignee', + tokenKey: TOKEN_TYPE_ASSIGNEE, value: __('Any'), }, { url: 'reviewer_id=None', - tokenKey: 'reviewer', + tokenKey: TOKEN_TYPE_REVIEWER, value: __('None'), }, { url: 'reviewer_id=Any', - tokenKey: 'reviewer', + tokenKey: TOKEN_TYPE_REVIEWER, value: __('Any'), }, { url: 'author_username=support-bot', - tokenKey: 'author', + tokenKey: TOKEN_TYPE_AUTHOR, value: 'support-bot', }, { url: 'milestone_title=None', - tokenKey: 'milestone', + tokenKey: TOKEN_TYPE_MILESTONE, value: __('None'), }, { url: 'milestone_title=Any', - tokenKey: 'milestone', + tokenKey: TOKEN_TYPE_MILESTONE, value: __('Any'), }, { url: 'milestone_title=%23upcoming', - tokenKey: 'milestone', + tokenKey: TOKEN_TYPE_MILESTONE, value: __('Upcoming'), }, { url: 'milestone_title=%23started', - tokenKey: 'milestone', + tokenKey: TOKEN_TYPE_MILESTONE, value: __('Started'), }, { url: 'release_tag=None', - tokenKey: 'release', + tokenKey: TOKEN_TYPE_RELEASE, value: __('None'), }, { url: 'release_tag=Any', - tokenKey: 'release', + tokenKey: TOKEN_TYPE_RELEASE, value: __('Any'), }, { url: 'label_name[]=None', - tokenKey: 'label', + tokenKey: TOKEN_TYPE_LABEL, value: __('None'), }, { url: 'label_name[]=Any', - tokenKey: 'label', + tokenKey: TOKEN_TYPE_LABEL, value: __('Any'), }, { url: 'my_reaction_emoji=None', - tokenKey: 'my-reaction', + tokenKey: TOKEN_TYPE_MY_REACTION, value: __('None'), }, { url: 'my_reaction_emoji=Any', - tokenKey: 'my-reaction', + tokenKey: TOKEN_TYPE_MY_REACTION, value: __('Any'), }, ].map((condition) => { diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js index 1ad2006d689..33fda7533e4 100644 --- a/app/assets/javascripts/filtered_search/visual_token_value.js +++ b/app/assets/javascripts/filtered_search/visual_token_value.js @@ -8,6 +8,7 @@ import { createAlert } from '~/flash'; import AjaxCache from '~/lib/utils/ajax_cache'; import UsersCache from '~/lib/utils/users_cache'; import { __ } from '~/locale'; +import { TOKEN_TYPE_LABEL } from '~/vue_shared/components/filtered_search_bar/constants'; export default class VisualTokenValue { constructor(tokenValue, tokenType, tokenOperator) { @@ -23,7 +24,7 @@ export default class VisualTokenValue { return; } - if (tokenType === 'label') { + if (tokenType === TOKEN_TYPE_LABEL) { this.updateLabelTokenColor(tokenValueContainer); } else if (USER_TOKEN_TYPES.includes(tokenType)) { this.updateUserTokenAppearance(tokenValueContainer, tokenValueElement); diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js index d177c67f133..4c9eb830ff6 100644 --- a/app/assets/javascripts/pages/projects/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -11,10 +11,14 @@ import { mergeUrlParams } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; import projectSelect from '~/project_select'; +const BRANCH_REF_TYPE = 'heads'; +const TAG_REF_TYPE = 'tags'; +const BRANCH_GROUP_NAME = __('Branches'); +const TAG_GROUP_NAME = __('Tags'); + export default class Project { constructor() { initClonePanel(); - // Ref switcher if (document.querySelector('.js-project-refs-dropdown')) { Project.initRefSwitcher(); @@ -62,6 +66,7 @@ export default class Project { return $('.js-project-refs-dropdown').each(function () { const $dropdown = $(this); const selected = $dropdown.data('selected'); + const refType = $dropdown.data('refType'); const fieldName = $dropdown.data('fieldName'); const shouldVisit = Boolean($dropdown.data('visit')); const $form = $dropdown.closest('form'); @@ -91,18 +96,32 @@ export default class Project { filterByText: true, inputFieldName: $dropdown.data('inputFieldName'), fieldName, - renderRow(ref) { + renderRow(ref, _, params) { const li = refListItem.cloneNode(false); const link = refLink.cloneNode(false); if (ref === selected) { - link.className = 'is-active'; + // Check group and current ref type to avoid adding a class when tags and branches share the same name + if ( + (refType === BRANCH_REF_TYPE && params.group === BRANCH_GROUP_NAME) || + (refType === TAG_REF_TYPE && params.group === TAG_GROUP_NAME) || + !refType + ) { + link.className = 'is-active'; + } } + link.textContent = ref; link.dataset.ref = ref; if (ref.length > 0 && shouldVisit) { - link.href = mergeUrlParams({ [fieldName]: ref }, linkTarget); + const urlParams = { [fieldName]: ref }; + if (params.group === BRANCH_GROUP_NAME) { + urlParams.ref_type = BRANCH_REF_TYPE; + } else { + urlParams.ref_type = TAG_REF_TYPE; + } + link.href = mergeUrlParams(urlParams, linkTarget); } li.appendChild(link); diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js index 8750e477803..e1f65375f25 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js @@ -53,6 +53,7 @@ export const SORT_DIRECTION = { export const FILTERED_SEARCH_LABELS = 'labels'; export const FILTERED_SEARCH_TERM = 'filtered-search-term'; +export const TOKEN_TITLE_APPROVED_BY = __('Approved-By'); export const TOKEN_TITLE_ASSIGNEE = s__('SearchToken|Assignee'); export const TOKEN_TITLE_AUTHOR = __('Author'); export const TOKEN_TITLE_CONFIDENTIAL = __('Confidential'); @@ -62,11 +63,13 @@ export const TOKEN_TITLE_MILESTONE = __('Milestone'); export const TOKEN_TITLE_MY_REACTION = __('My-Reaction'); export const TOKEN_TITLE_ORGANIZATION = s__('Crm|Organization'); export const TOKEN_TITLE_RELEASE = __('Release'); +export const TOKEN_TITLE_REVIEWER = s__('SearchToken|Reviewer'); export const TOKEN_TITLE_SOURCE_BRANCH = __('Source Branch'); export const TOKEN_TITLE_STATUS = __('Status'); export const TOKEN_TITLE_TARGET_BRANCH = __('Target Branch'); export const TOKEN_TITLE_TYPE = __('Type'); +export const TOKEN_TYPE_APPROVED_BY = 'approved-by'; export const TOKEN_TYPE_ASSIGNEE = 'assignee'; export const TOKEN_TYPE_AUTHOR = 'author'; export const TOKEN_TYPE_CONFIDENTIAL = 'confidential'; @@ -83,5 +86,8 @@ export const TOKEN_TYPE_MILESTONE = 'milestone'; export const TOKEN_TYPE_MY_REACTION = 'my-reaction'; export const TOKEN_TYPE_ORGANIZATION = 'organization'; export const TOKEN_TYPE_RELEASE = 'release'; +export const TOKEN_TYPE_REVIEWER = 'reviewer'; +export const TOKEN_TYPE_SOURCE_BRANCH = 'source-branch'; +export const TOKEN_TYPE_TARGET_BRANCH = 'target-branch'; export const TOKEN_TYPE_TYPE = 'type'; export const TOKEN_TYPE_WEIGHT = 'weight'; diff --git a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue index 4225509dd2c..2cdff901978 100644 --- a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue +++ b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue @@ -1,5 +1,5 @@ <script> -import { GlButton } from '@gitlab/ui'; +import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import $ from 'jquery'; import '~/behaviors/markdown/render_gfm'; import SafeHtml from '~/vue_shared/directives/safe_html'; @@ -9,6 +9,7 @@ const isCheckbox = (target) => target?.classList.contains('task-list-item-checkb export default { directives: { SafeHtml, + GlTooltip: GlTooltipDirective, }, components: { GlButton, @@ -98,10 +99,12 @@ export default { <label class="d-block col-form-label gl-mr-5">{{ __('Description') }}</label> <gl-button v-if="canEdit" + v-gl-tooltip class="gl-ml-auto" icon="pencil" data-testid="edit-description" :aria-label="__('Edit description')" + :title="__('Edit description')" @click="$emit('startEditing')" /> </div> diff --git a/app/controllers/concerns/integrations/params.rb b/app/controllers/concerns/integrations/params.rb index 30de4a86bec..74d998503b7 100644 --- a/app/controllers/concerns/integrations/params.rb +++ b/app/controllers/concerns/integrations/params.rb @@ -88,7 +88,9 @@ module Integrations param_values = return_value[:integration] if param_values.is_a?(ActionController::Parameters) - if action_name == 'update' && integration.chat? && param_values['webhook'] == BaseChatNotification::SECRET_MASK + if %w[update test].include?(action_name) && integration.chat? && + param_values['webhook'] == BaseChatNotification::SECRET_MASK + param_values.delete('webhook') end diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index f4125fd0a15..dd900173c40 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -28,6 +28,8 @@ class Projects::CommitsController < Projects::ApplicationController @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened .find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref) + @ref_type = ref_type + respond_to do |format| format.html format.atom { render layout: 'xml' } @@ -73,18 +75,20 @@ class Projects::CommitsController < Projects::ApplicationController search = permitted_params[:search] author = permitted_params[:author] + # fully_qualified_ref is available in some situations when the use_ref_type_parameter FF is enabled + ref = @fully_qualified_ref || @ref @commits = if search.present? - @repository.find_commits_by_message(search, @ref, @path, @limit, @offset) + @repository.find_commits_by_message(search, ref, @path, @limit, @offset) elsif author.present? - @repository.commits(@ref, author: author, path: @path, limit: @limit, offset: @offset) + @repository.commits(ref, author: author, path: @path, limit: @limit, offset: @offset) else - @repository.commits(@ref, path: @path, limit: @limit, offset: @offset) + @repository.commits(ref, path: @path, limit: @limit, offset: @offset) end @commits.each(&:lazy_author) # preload authors - @commits = @commits.with_markdown_cache.with_latest_pipeline(@ref) + @commits = @commits.with_markdown_cache.with_latest_pipeline(ref) @commits = set_commits_for_rendering(@commits) end diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index 72af3280a39..05fe34ceb5b 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -34,7 +34,11 @@ class Projects::RefsController < Projects::ApplicationController when "badges" project_settings_ci_cd_path(@project, ref: @id) else - project_commits_path(@project, @id) + if Feature.enabled?(:use_ref_type_parameter, @project) + project_commits_path(@project, @id, ref_type: ref_type) + else + project_commits_path(@project, @id) + end end redirect_to new_path diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index e41a3fa5091..b16f44adeb6 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -480,6 +480,28 @@ module ProjectsHelper format_cached_count(1000, number) end + def fork_divergence_message(counts) + messages = [] + + if counts[:behind] > 0 + messages << s_("ForksDivergence|%{behind} %{commit_word} behind") % { + behind: counts[:behind], commit_word: n_('commit', 'commits', counts[:behind]) + } + end + + if counts[:ahead] > 0 + messages << s_("ForksDivergence|%{ahead} %{commit_word} ahead of") % { + ahead: counts[:ahead], commit_word: n_('commit', 'commits', counts[:ahead]) + } + end + + if messages.blank? + s_('ForksDivergence|Up to date with upstream repository') + else + s_("ForksDivergence|%{messages} upstream repository") % { messages: messages.join(', ') } + end + end + private def localized_access_names diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb index 9002fdda128..cbee02a28c0 100644 --- a/app/helpers/sidebars_helper.rb +++ b/app/helpers/sidebars_helper.rb @@ -20,9 +20,8 @@ module SidebarsHelper end end - def project_sidebar_context(project, user, current_ref) - context_data = project_sidebar_context_data(project, user, current_ref) - + def project_sidebar_context(project, user, current_ref, ref_type: nil) + context_data = project_sidebar_context_data(project, user, current_ref, ref_type: ref_type) Sidebars::Projects::Context.new(**context_data) end @@ -83,12 +82,13 @@ module SidebarsHelper tracking_attrs('user_side_navigation', 'render', 'user_side_navigation') end - def project_sidebar_context_data(project, user, current_ref) + def project_sidebar_context_data(project, user, current_ref, ref_type: nil) { current_user: user, container: project, learn_gitlab_enabled: learn_gitlab_enabled?(project), current_ref: current_ref, + ref_type: ref_type, jira_issues_integration: project_jira_issues_integration?, can_view_pipeline_editor: can_view_pipeline_editor?(project), show_cluster_hint: show_gke_cluster_integration_callout?(project) diff --git a/app/models/projects/forks/divergence_counts.rb b/app/models/projects/forks/divergence_counts.rb new file mode 100644 index 00000000000..0831d9cbc7e --- /dev/null +++ b/app/models/projects/forks/divergence_counts.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Projects + module Forks + # Class for calculating the divergence of a fork with the source project + class DivergenceCounts + EXPIRATION_TIME = 8.hours + + def initialize(project, ref) + @project = project + @fork_repo = project.repository + @source_repo = project.fork_source.repository + @ref = ref + end + + def counts + ahead, behind = calculate_divergence_counts + + { ahead: ahead.to_i, behind: behind.to_i } + end + + private + + attr_reader :project, :fork_repo, :source_repo, :ref + + def cache_key + @cache_key ||= ['project_forks', project.id, ref, 'divergence_counts'] + end + + def calculate_divergence_counts + fork_sha = fork_repo.commit(ref).sha + source_sha = source_repo.commit.sha + + cached_source_sha, cached_fork_sha, counts = Rails.cache.read(cache_key) + return counts if counts.present? && cached_source_sha == source_sha && cached_fork_sha == fork_sha + + counts = + Gitlab::Git::CrossRepo.new(fork_repo, source_repo) + .execute(source_sha) do |cross_repo_sha| + fork_repo.count_commits_between(fork_sha, cross_repo_sha, left_right: true) + end + + Rails.cache.write(cache_key, [source_sha, fork_sha, counts], expires_in: EXPIRATION_TIME) + + counts + end + end + end +end diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index a06f9f8d6ef..67c3cd9cc54 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -1 +1 @@ -= render partial: 'shared/nav/sidebar', object: Sidebars::Projects::Panel.new(project_sidebar_context(@project, current_user, current_ref)) += render partial: 'shared/nav/sidebar', object: Sidebars::Projects::Panel.new(project_sidebar_context(@project, current_user, current_ref, ref_type: @ref_type)) diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml index 51222784847..8bf397d0796 100644 --- a/app/views/projects/_files.html.haml +++ b/app/views/projects/_files.html.haml @@ -10,6 +10,9 @@ .nav-block.gl-display-flex.gl-xs-flex-direction-column.gl-align-items-stretch = render 'projects/tree/tree_header', tree: @tree, is_project_overview: is_project_overview + - if project.forked? && Feature.enabled?(:fork_divergence_counts, @project.fork_source) + = render 'projects/fork_info' + .info-well.gl-display-none.gl-sm-display-flex.project-last-commit.gl-flex-direction-column #js-last-commit.gl-m-auto = gl_loading_icon(size: 'md') diff --git a/app/views/projects/_fork_info.html.haml b/app/views/projects/_fork_info.html.haml new file mode 100644 index 00000000000..834126f985c --- /dev/null +++ b/app/views/projects/_fork_info.html.haml @@ -0,0 +1,13 @@ +.info-well.gl-sm-display-flex.gl-flex-direction-column + .well-segment.gl-p-5.gl-w-full.gl-display-flex + .gl-icon.s32.gl-mt-4.gl-mr-4.gl-text-center + = sprite_icon('fork') + %div + - source = visible_fork_source(@project) + - if source + #{ s_('ForkedFromProjectPath|Forked from') } + = link_to source.full_name, project_path(source), data: { qa_selector: 'forked_from_link' } + .gl-text-secondary + = fork_divergence_message(::Projects::Forks::DivergenceCounts.new(@project, @ref).counts) + - else + = s_('ForkedFromProjectPath|Forked from an inaccessible project') diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 3b240ee60ed..33ae6104d84 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -53,7 +53,7 @@ %button.btn.gl-button.btn-blank.btn-link.js-read-more-trigger.d-lg-none{ type: "button" } = _("Read more") - - if @project.forked? + - if @project.forked? && Feature.disabled?(:fork_divergence_counts, @project.fork_source) %p - source = visible_fork_source(@project) - if source diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index ae68a13929e..765b4e7b615 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -1,6 +1,7 @@ - breadcrumb_title _("Commits") - add_page_specific_style 'page_bundles/tree' - page_title _("Commits"), @ref + = content_for :meta_tags do = auto_discovery_link_tag(:atom, project_commits_path(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits") @@ -24,7 +25,7 @@ = _("Create merge request") .control - = form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form js-signature-container', data: { 'signatures-path' => namespace_project_signatures_path }) do + = form_tag(project_commits_path(@project, @id, ref_type: @ref_type), method: :get, class: 'commits-search-form js-signature-container', data: { 'signatures-path' => namespace_project_signatures_path(ref_type: @ref_type)}) do = search_field_tag :search, params[:search], { placeholder: _('Search by message'), id: 'commits-search', class: 'form-control gl-form-input input-short gl-mt-3 gl-sm-mt-0 gl-min-w-full', spellcheck: false } .control.d-none.d-md-block = link_to project_commits_path(@project, @id, rss_url_options), title: _("Commits feed"), class: 'btn gl-button btn-default btn-icon' do diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index 20bf2141cc3..6a36f85daa4 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -2,7 +2,7 @@ - ref = local_assigns.fetch(:ref, @ref) - form_path = local_assigns.fetch(:form_path, switch_project_refs_path(@project)) -- dropdown_toggle_text = ref || @project.default_branch +- dropdown_toggle_text = @id || @project.default_branch - field_name = local_assigns.fetch(:field_name, 'ref') = form_tag form_path, method: :get, class: "project-refs-form" do @@ -13,7 +13,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, sort: 'updated_desc'), field_name: field_name, submit_form_on_click: true, visit: true, qa_selector: "branches_dropdown", testid: "branches-select" }, { toggle_class: "js-project-refs-dropdown" } + = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: ref, ref_type: @ref_type, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: field_name, submit_form_on_click: true, visit: true, qa_selector: "branches_dropdown", testid: "branches-select" }, { toggle_class: "js-project-refs-dropdown" } .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging{ class: ("dropdown-menu-right" if local_assigns[:align_right]), data: { qa_selector: "branches_dropdown_content" } } .dropdown-page-one = dropdown_title _("Switch branch/tag") |