summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-11-21 18:07:57 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-11-21 18:07:57 +0000
commitc0b718a0dbd99e6c0d30e5bc55bdcf4a12946375 (patch)
tree8ad3691912d91d8cf7b3931f68a4284ae7b5995c /app
parent5dc70663c4ff1feb215428ce50673b5b646f9809 (diff)
downloadgitlab-ce-c0b718a0dbd99e6c0d30e5bc55bdcf4a12946375.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/render.js2
-rw-r--r--app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js31
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js34
-rw-r--r--app/assets/javascripts/filtered_search/constants.js15
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js3
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js12
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js51
-rw-r--r--app/assets/javascripts/filtered_search/visual_token_value.js3
-rw-r--r--app/assets/javascripts/pages/projects/project.js27
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description_rendered.vue5
-rw-r--r--app/controllers/concerns/integrations/params.rb4
-rw-r--r--app/controllers/projects/commits_controller.rb12
-rw-r--r--app/controllers/projects/refs_controller.rb6
-rw-r--r--app/helpers/projects_helper.rb22
-rw-r--r--app/helpers/sidebars_helper.rb8
-rw-r--r--app/models/projects/forks/divergence_counts.rb49
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml2
-rw-r--r--app/views/projects/_files.html.haml3
-rw-r--r--app/views/projects/_fork_info.html.haml13
-rw-r--r--app/views/projects/_home_panel.html.haml2
-rw-r--r--app/views/projects/commits/show.html.haml3
-rw-r--r--app/views/shared/_ref_switcher.html.haml4
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")