Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-12-01 12:09:17 +00:00
parent a7704bf16a
commit 7b2635a55d
126 changed files with 1838 additions and 1749 deletions

View File

@ -214,7 +214,6 @@ linters:
- 'app/views/projects/mattermosts/_team_selection.html.haml'
- 'app/views/projects/mattermosts/new.html.haml'
- 'app/views/projects/merge_requests/_commits.html.haml'
- 'app/views/projects/merge_requests/_how_to_merge.html.haml'
- 'app/views/projects/merge_requests/_mr_title.html.haml'
- 'app/views/projects/merge_requests/conflicts/_commit_stats.html.haml'
- 'app/views/projects/merge_requests/conflicts/_file_actions.html.haml'

View File

@ -1 +1 @@
9677be26e1a7f7e4f56bf4f7ba47bc0d16f40e16
ae38755015839b67292e5f550a9a4fd72c3c1a56

View File

@ -1,5 +1,5 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import { mapActions, mapGetters, mapState } from 'vuex';
import { cloneDeep } from 'lodash';
import {
GlDropdownItem,
@ -75,6 +75,7 @@ export default {
},
computed: {
...mapGetters(['activeIssue']),
...mapState(['isSettingAssignees']),
assigneeText() {
return n__('Assignee', '%d Assignees', this.selected.length);
},
@ -131,7 +132,7 @@ export default {
</script>
<template>
<board-editable-item :title="assigneeText" @close="saveAssignees">
<board-editable-item :loading="isSettingAssignees" :title="assigneeText" @close="saveAssignees">
<template #collapsed>
<issuable-assignees :users="selected" @assign-self="assignSelf" />
</template>

View File

@ -320,6 +320,8 @@ export default {
},
setAssignees: ({ commit, getters }, assigneeUsernames) => {
commit(types.SET_ASSIGNEE_LOADING, true);
return gqlClient
.mutate({
mutation: updateAssignees,
@ -339,6 +341,9 @@ export default {
});
return nodes;
})
.finally(() => {
commit(types.SET_ASSIGNEE_LOADING, false);
});
},

View File

@ -34,4 +34,5 @@ export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE';
export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE';
export const SET_ACTIVE_ID = 'SET_ACTIVE_ID';
export const UPDATE_ISSUE_BY_ID = 'UPDATE_ISSUE_BY_ID';
export const SET_ASSIGNEE_LOADING = 'SET_ASSIGNEE_LOADING';
export const RESET_ISSUES = 'RESET_ISSUES';

View File

@ -143,6 +143,10 @@ export default {
Vue.set(state.issues[issueId], prop, value);
},
[mutationTypes.SET_ASSIGNEE_LOADING](state, isLoading) {
state.isSettingAssignees = isLoading;
},
[mutationTypes.REQUEST_ADD_ISSUE]: () => {
notImplemented();
},

View File

@ -11,6 +11,7 @@ export default () => ({
boardLists: {},
listsFlags: {},
issuesByListId: {},
isSettingAssignees: false,
pageInfoByListId: {},
issues: {},
filterParams: {},

View File

@ -1,92 +0,0 @@
import DropLab from './droplab/drop_lab';
import ISetter from './droplab/plugins/input_setter';
// Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = { ...ISetter };
class CloseReopenReportToggle {
constructor(opts = {}) {
this.dropdownTrigger = opts.dropdownTrigger;
this.dropdownList = opts.dropdownList;
this.button = opts.button;
}
initDroplab() {
this.reopenItem = this.dropdownList.querySelector('.reopen-item');
this.closeItem = this.dropdownList.querySelector('.close-item');
this.droplab = new DropLab();
const config = this.setConfig();
this.droplab.init(this.dropdownTrigger, this.dropdownList, [InputSetter], config);
}
updateButton(isClosed) {
this.toggleButtonType(isClosed);
this.button.blur();
}
toggleButtonType(isClosed) {
const [showItem, hideItem] = this.getButtonTypes(isClosed);
showItem.classList.remove('hidden');
showItem.classList.add('droplab-item-selected');
hideItem.classList.add('hidden');
hideItem.classList.remove('droplab-item-selected');
showItem.click();
}
getButtonTypes(isClosed) {
return isClosed ? [this.reopenItem, this.closeItem] : [this.closeItem, this.reopenItem];
}
setDisable(shouldDisable = true) {
if (shouldDisable) {
this.button.setAttribute('disabled', 'true');
this.dropdownTrigger.setAttribute('disabled', 'true');
} else {
this.button.removeAttribute('disabled');
this.dropdownTrigger.removeAttribute('disabled');
}
}
setConfig() {
const config = {
InputSetter: [
{
input: this.button,
valueAttribute: 'data-text',
inputAttribute: 'data-value',
},
{
input: this.button,
valueAttribute: 'data-text',
inputAttribute: 'title',
},
{
input: this.button,
valueAttribute: 'data-button-class',
inputAttribute: 'class',
},
{
input: this.dropdownTrigger,
valueAttribute: 'data-toggle-class',
inputAttribute: 'class',
},
{
input: this.button,
valueAttribute: 'data-url',
inputAttribute: 'data-endpoint',
},
],
};
return config;
}
}
export default CloseReopenReportToggle;

View File

@ -1,5 +1,5 @@
<script>
import { GlButton, GlAlert } from '@gitlab/ui';
import { GlButton, GlAlert, GlModalDirective } from '@gitlab/ui';
import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants';
export default {
@ -7,6 +7,9 @@ export default {
GlAlert,
GlButton,
},
directives: {
GlModalDirective,
},
props: {
limited: {
type: Boolean,
@ -60,9 +63,8 @@ export default {
</gl-button>
<gl-button
v-if="mergeable"
v-gl-modal-directive="'modal-merge-info'"
class="gl-alert-action"
data-toggle="modal"
data-target="#modal_merge_info"
>
{{ __('Merge locally') }}
</gl-button>

View File

@ -1,27 +0,0 @@
import CloseReopenReportToggle from '../close_reopen_report_toggle';
function initCloseReopenReport() {
const container = document.querySelector('.js-issuable-close-dropdown');
if (!container) return undefined;
const dropdownTrigger = container.querySelector('.js-issuable-close-toggle');
const dropdownList = container.querySelector('.js-issuable-close-menu');
const button = container.querySelector('.js-issuable-close-button');
const closeReopenReportToggle = new CloseReopenReportToggle({
dropdownTrigger,
dropdownList,
button,
});
closeReopenReportToggle.initDroplab();
return closeReopenReportToggle;
}
const IssuablesHelper = {
initCloseReopenReport,
};
export default IssuablesHelper;

View File

@ -1,15 +0,0 @@
import $ from 'jquery';
export default () => {
const modal = $('#modal_merge_info');
if (modal) {
modal.modal({
modal: true,
show: false,
});
$('.how_to_merge_link').on('click', modal.show);
$('.modal-header .close').on('click', modal.hide);
}
};

View File

@ -112,7 +112,7 @@ export default {
</div>
<div
data-testid="header-actions"
class="detail-page-header-actions js-issuable-actions js-issuable-buttons gl-display-flex gl-display-md-block"
class="detail-page-header-actions gl-display-flex gl-display-md-block"
>
<slot name="header-actions"></slot>
</div>

View File

@ -1,41 +1,22 @@
/* eslint-disable consistent-return */
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
import { addDelimiter } from './lib/utils/text_utility';
import { deprecatedCreateFlash as flash } from './flash';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper';
import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from './locale';
export default class Issue {
constructor() {
if ($('.btn-close, .btn-reopen').length) this.initIssueBtnEventListeners();
if ($('.js-close-blocked-issue-warning').length) this.initIssueWarningBtnEventListener();
if ($('.js-alert-moved-from-service-desk-warning').length) {
const trimmedPathname = window.location.pathname.slice(1);
this.alertMovedFromServiceDeskDismissedKey = joinPaths(
trimmedPathname,
'alert-issue-moved-from-service-desk-dismissed',
);
this.initIssueMovedFromServiceDeskDismissHandler();
Issue.initIssueMovedFromServiceDeskDismissHandler();
}
Issue.$btnNewBranch = $('#new-branch');
Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap');
if (document.querySelector('#related-branches')) {
Issue.initRelatedBranches();
}
this.closeButtons = $('.btn-close');
this.reopenButtons = $('.btn-reopen');
this.initCloseReopenReport();
Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap');
if (Issue.createMrDropdownWrap) {
this.createMergeRequestDropdown = new CreateMergeRequestDropdown(Issue.createMrDropdownWrap);
@ -71,7 +52,6 @@ export default class Issue {
isOpenBadge.toggleClass('hidden', isClosed);
$(document).trigger('issuable:change', isClosed);
this.toggleCloseReopenButton(isClosed);
let numProjectIssues = Number(
projectIssuesCounter
@ -91,104 +71,16 @@ export default class Issue {
}
}
initIssueBtnEventListeners() {
const issueFailMessage = __('Unable to update this issue at this time.');
$('.report-abuse-link').on('click', e => {
// this is needed because of the implementation of
// the dropdown toggle and Report Abuse needing to be
// linked to another page.
e.stopPropagation();
});
// NOTE: data attribute seems unnecessary but is actually necessary
return $('.js-issuable-buttons[data-action="close-reopen"]').on(
'click',
'.btn-close, .btn-reopen, .btn-close-anyway',
e => {
e.preventDefault();
e.stopImmediatePropagation();
const $button = $(e.currentTarget);
const shouldSubmit = $button.hasClass('btn-comment');
if (shouldSubmit) {
Issue.submitNoteForm($button.closest('form'));
}
const shouldDisplayBlockedWarning = $button.hasClass('btn-issue-blocked');
const warningBanner = $('.js-close-blocked-issue-warning');
if (shouldDisplayBlockedWarning) {
this.toggleWarningAndCloseButton();
} else {
this.disableCloseReopenButton($button);
const url = $button.data('endpoint');
return axios
.put(url)
.then(({ data }) => {
const isClosed = $button.is('.btn-close, .btn-close-anyway');
this.updateTopState(isClosed, data);
if ($button.hasClass('btn-close-anyway')) {
warningBanner.addClass('hidden');
if (this.closeReopenReportToggle)
$('.js-issuable-close-dropdown').removeClass('hidden');
}
})
.catch(() => flash(issueFailMessage))
.then(() => {
this.disableCloseReopenButton($button, false);
});
}
},
);
}
initCloseReopenReport() {
this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport();
if (this.closeButtons) this.closeButtons = this.closeButtons.not('.issuable-close-button');
if (this.reopenButtons) this.reopenButtons = this.reopenButtons.not('.issuable-close-button');
}
disableCloseReopenButton($button, shouldDisable) {
if (this.closeReopenReportToggle) {
this.closeReopenReportToggle.setDisable(shouldDisable);
} else {
$button.prop('disabled', shouldDisable);
}
}
toggleCloseReopenButton(isClosed) {
if (this.closeReopenReportToggle) this.closeReopenReportToggle.updateButton(isClosed);
this.closeButtons.toggleClass('hidden', isClosed);
this.reopenButtons.toggleClass('hidden', !isClosed);
}
toggleWarningAndCloseButton() {
const warningBanner = $('.js-close-blocked-issue-warning');
warningBanner.toggleClass('hidden');
$('.btn-close').toggleClass('hidden');
if (this.closeReopenReportToggle) {
$('.js-issuable-close-dropdown').toggleClass('hidden');
}
}
initIssueWarningBtnEventListener() {
return $(document).on(
'click',
'.js-close-blocked-issue-warning .js-cancel-blocked-issue-warning',
e => {
e.preventDefault();
e.stopImmediatePropagation();
this.toggleWarningAndCloseButton();
},
);
}
initIssueMovedFromServiceDeskDismissHandler() {
static initIssueMovedFromServiceDeskDismissHandler() {
const alertMovedFromServiceDeskWarning = $('.js-alert-moved-from-service-desk-warning');
if (!localStorage.getItem(this.alertMovedFromServiceDeskDismissedKey)) {
const trimmedPathname = window.location.pathname.slice(1);
const alertMovedFromServiceDeskDismissedKey = joinPaths(
trimmedPathname,
'alert-issue-moved-from-service-desk-dismissed',
);
if (!localStorage.getItem(alertMovedFromServiceDeskDismissedKey)) {
alertMovedFromServiceDeskWarning.show();
}
@ -196,20 +88,13 @@ export default class Issue {
e.preventDefault();
e.stopImmediatePropagation();
alertMovedFromServiceDeskWarning.remove();
localStorage.setItem(this.alertMovedFromServiceDeskDismissedKey, true);
localStorage.setItem(alertMovedFromServiceDeskDismissedKey, true);
});
}
static submitNoteForm(form) {
const noteText = form.find('textarea.js-note-text').val();
if (noteText && noteText.trim().length > 0) {
return form.submit();
}
}
static initRelatedBranches() {
const $container = $('#related-branches');
return axios
axios
.get($container.data('url'))
.then(({ data }) => {
if ('html' in data) {

View File

@ -2,7 +2,6 @@ import ZenMode from '~/zen_mode';
import initIssuableSidebar from '~/init_issuable_sidebar';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import { handleLocationHash } from '~/lib/utils/common_utils';
import howToMerge from '~/how_to_merge';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initSourcegraph from '~/sourcegraph';
import loadAwardsHandler from '~/awards_handler';
@ -15,7 +14,6 @@ export default function() {
initPipelines();
new ShortcutsIssuable(true); // eslint-disable-line no-new
handleLocationHash();
howToMerge();
initSourcegraph();
loadAwardsHandler();
initInviteMemberModal();

View File

@ -1,8 +1,15 @@
<script>
import { GlButton, GlModalDirective } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
export default {
name: 'MRWidgetMergeHelp',
components: {
GlButton,
},
directives: {
GlModalDirective,
},
props: {
missingBranch: {
type: String,
@ -31,13 +38,12 @@ export default {
{{ s__('mrWidget|You can merge this merge request manually using the') }}
</template>
<button
type="button"
class="btn-link btn-blank js-open-modal-help"
data-toggle="modal"
data-target="#modal_merge_info"
<gl-button
v-gl-modal-directive="'modal-merge-info'"
variant="link"
class="gl-mt-n2 js-open-modal-help"
>
{{ s__('mrWidget|command line') }}
</button>
</gl-button>
</section>
</template>

View File

@ -887,11 +887,6 @@
}
}
.issuable-close-button,
.issuable-close-toggle {
@include transition(border-color, color);
}
.issuable-close-dropdown {
.dropdown-menu {
min-width: 270px;

View File

@ -507,19 +507,6 @@ $mr-widget-min-height: 69px;
display: none;
}
#modal_merge_info .modal-dialog {
.dark {
margin-right: 40px;
}
.btn-clipboard {
margin-right: 20px;
margin-top: 5px;
position: absolute;
right: 0;
}
}
.mr-links {
padding-left: $gl-padding-8 + $status-icon-size + $gl-btn-padding;

View File

@ -73,6 +73,8 @@ class Projects::IssuesController < Projects::ApplicationController
feature_category :service_desk, [:service_desk]
feature_category :importers, [:import_csv, :export_csv]
attr_accessor :vulnerability_id
def index
@issues = @issuables
@ -124,6 +126,8 @@ class Projects::IssuesController < Projects::ApplicationController
service = ::Issues::CreateService.new(project, current_user, create_params)
@issue = service.execute
create_vulnerability_issue_link(issue)
if service.discussions_to_resolve.count(&:resolved?) > 0
flash[:notice] = if service.discussion_to_resolve_id
_("Resolved 1 discussion.")
@ -384,6 +388,9 @@ class Projects::IssuesController < Projects::ApplicationController
def service_desk?
action_name == 'service_desk'
end
# Overridden in EE
def create_vulnerability_issue_link(issue); end
end
Projects::IssuesController.prepend_if_ee('EE::Projects::IssuesController')

View File

@ -87,8 +87,12 @@ module Repositories
@project
end
def repository_path
@repository_path ||= params[:repository_path]
end
def parse_repo_path
@container, @project, @repo_type, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:repository_id]}")
@container, @project, @repo_type, @redirected_path = Gitlab::RepoPath.parse(repository_path)
end
def render_missing_personal_access_token

View File

@ -90,7 +90,6 @@ module Repositories
def access
@access ||= access_klass.new(access_actor, container, 'http',
authentication_abilities: authentication_abilities,
namespace_path: params[:namespace_id],
repository_path: repository_path,
redirected_path: redirected_path,
auth_result_type: auth_result_type)
@ -113,10 +112,6 @@ module Repositories
@access_klass ||= repo_type.access_checker_class
end
def repository_path
@repository_path ||= params[:repository_id].sub(/\.git$/, '')
end
def log_user_activity
Users::ActivityService.new(user).execute
end

View File

@ -33,6 +33,8 @@ class UsersController < ApplicationController
end
format.json do
# In 13.8, this endpoint will be removed:
# https://gitlab.com/gitlab-org/gitlab/-/issues/289972
load_events
pager_json("events/_events", @events.count, events: @events)
end
@ -42,6 +44,11 @@ class UsersController < ApplicationController
def activity
respond_to do |format|
format.html { render 'show' }
format.json do
load_events
pager_json("events/_events", @events.count, events: @events)
end
end
end

View File

@ -21,7 +21,7 @@ class GroupMembersFinder < UnionFinder
group_members = group_members_list
relations = []
return group_members if include_relations == [:direct]
return filter_members(group_members) if include_relations == [:direct]
relations << group_members if include_relations.include?(:direct)

View File

@ -38,10 +38,11 @@ module Types
field :user,
Types::UserType,
null: false,
description: 'The user who awarded the emoji',
resolve: -> (award_emoji, _args, _context) {
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, award_emoji.user_id).find
}
description: 'The user who awarded the emoji'
def user
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.user_id).find
end
end
end
end

View File

@ -19,8 +19,7 @@ module Types
field :label, Types::LabelType, null: true,
description: 'Label of the list'
field :collapsed, GraphQL::BOOLEAN_TYPE, null: true,
description: 'Indicates if list is collapsed for this user',
resolve: -> (list, _args, ctx) { list.collapsed?(ctx[:current_user]) }
description: 'Indicates if list is collapsed for this user'
field :issues_count, GraphQL::INT_TYPE, null: true,
description: 'Count of issues in the list'
@ -32,6 +31,10 @@ module Types
metadata[:size]
end
def collapsed
object.collapsed?(context[:current_user])
end
def metadata
strong_memoize(:metadata) do
list = self.object

View File

@ -25,20 +25,21 @@ module Types
description: 'Tooltip associated with the status',
method: :status_tooltip
field :action, Types::Ci::StatusActionType, null: true,
description: 'Action information for the status. This includes method, button title, icon, path, and title',
resolve: -> (obj, _args, _ctx) {
if obj.has_action?
{
button_title: obj.action_button_title,
icon: obj.action_icon,
method: obj.action_method,
path: obj.action_path,
title: obj.action_title
}
else
nil
end
}
description: 'Action information for the status. This includes method, button title, icon, path, and title'
def action
if object.has_action?
{
button_title: object.action_button_title,
icon: object.action_icon,
method: object.action_method,
path: object.action_path,
title: object.action_title
}
else
nil
end
end
end
# rubocop: enable Graphql/AuthorizeTypes
end

View File

@ -13,8 +13,11 @@ module Types
field :jobs, Ci::JobType.connection_type, null: true,
description: 'Jobs in group'
field :detailed_status, Types::Ci::DetailedStatusType, null: true,
description: 'Detailed status of the group',
resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) }
description: 'Detailed status of the group'
def detailed_status
object.detailed_status(context[:current_user])
end
end
end
end

View File

@ -7,25 +7,26 @@ module Types
graphql_name 'CiJob'
field :pipeline, Types::Ci::PipelineType, null: false,
description: 'Pipeline the job belongs to',
resolve: -> (build, _, _) { Gitlab::Graphql::Loaders::BatchModelLoader.new(::Ci::Pipeline, build.pipeline_id).find }
description: 'Pipeline the job belongs to'
field :name, GraphQL::STRING_TYPE, null: true,
description: 'Name of the job'
description: 'Name of the job'
field :needs, JobType.connection_type, null: true,
description: 'Builds that must complete before the jobs run'
description: 'Builds that must complete before the jobs run'
field :detailed_status, Types::Ci::DetailedStatusType, null: true,
description: 'Detailed status of the job',
resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) }
description: 'Detailed status of the job'
field :scheduled_at, Types::TimeType, null: true,
description: 'Schedule for the build'
description: 'Schedule for the build'
field :artifacts, Types::Ci::JobArtifactType.connection_type, null: true,
description: 'Artifacts generated by the job'
def pipeline
Gitlab::Graphql::Loaders::BatchModelLoader.new(::Ci::Pipeline, object.pipeline_id).find
end
def detailed_status
object.detailed_status(context[:current_user])
end
def artifacts
if object.is_a?(::Ci::Build)
object.job_artifacts

View File

@ -27,8 +27,7 @@ module Types
description: "Status of the pipeline (#{::Ci::Pipeline.all_state_names.compact.join(', ').upcase})"
field :detailed_status, Types::Ci::DetailedStatusType, null: false,
description: 'Detailed status of the pipeline',
resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) }
description: 'Detailed status of the pipeline'
field :config_source, PipelineConfigSourceEnum, null: true,
description: "Config source of the pipeline (#{::Enums::Ci::Pipeline.config_sources.keys.join(', ').upcase})"
@ -60,8 +59,7 @@ module Types
resolver: Resolvers::Ci::PipelineStagesResolver
field :user, Types::UserType, null: true,
description: 'Pipeline user',
resolve: -> (pipeline, _args, _context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, pipeline.user_id).find }
description: 'Pipeline user'
field :retryable, GraphQL::BOOLEAN_TYPE,
description: 'Specifies if a pipeline can be retried',
@ -91,11 +89,22 @@ module Types
method: :triggered_by_pipeline
field :path, GraphQL::STRING_TYPE, null: true,
description: "Relative path to the pipeline's page",
resolve: -> (obj, _args, _ctx) { ::Gitlab::Routing.url_helpers.project_pipeline_path(obj.project, obj) }
description: "Relative path to the pipeline's page"
field :project, Types::ProjectType, null: true,
description: 'Project the pipeline belongs to'
def detailed_status
object.detailed_status(context[:current_user])
end
def user
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.user_id).find
end
def path
::Gitlab::Routing.url_helpers.project_pipeline_path(object.project, object)
end
end
end
end

View File

@ -11,8 +11,11 @@ module Types
field :groups, Ci::GroupType.connection_type, null: true,
description: 'Group of jobs for the stage'
field :detailed_status, Types::Ci::DetailedStatusType, null: true,
description: 'Detailed status of the stage',
resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) }
description: 'Detailed status of the stage'
def detailed_status
object.detailed_status(context[:current_user])
end
end
end
end

View File

@ -31,10 +31,7 @@ module Types
field :author_name, type: GraphQL::STRING_TYPE, null: true,
description: 'Commit authors name'
field :author_gravatar, type: GraphQL::STRING_TYPE, null: true,
description: 'Commit authors gravatar',
resolve: -> (commit, args, context) do
GravatarService.new.execute(commit.author_email, 40)
end
description: 'Commit authors gravatar'
# models/commit lazy loads the author by email
field :author, type: Types::UserType, null: true,
@ -44,5 +41,9 @@ module Types
null: true,
description: 'Pipelines of the commit ordered latest first',
resolver: Resolvers::CommitPipelinesResolver
def author_gravatar
GravatarService.new.execute(object.author_email, 40)
end
end
end

View File

@ -11,7 +11,10 @@ module Types
description 'Represents a Group Invitation'
field :group, Types::GroupType, null: true,
description: 'Group that a User is invited to',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, obj.source_id).find }
description: 'Group that a User is invited to'
def group
Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, object.source_id).find
end
end
end

View File

@ -11,7 +11,10 @@ module Types
description 'Represents a Group Membership'
field :group, Types::GroupType, null: true,
description: 'Group that a User is a member of',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, obj.source_id).find }
description: 'Group that a User is a member of'
def group
Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, object.source_id).find
end
end
end

View File

@ -12,10 +12,7 @@ module Types
description: 'Web URL of the group'
field :avatar_url, GraphQL::STRING_TYPE, null: true,
description: 'Avatar URL of the group',
resolve: -> (group, args, ctx) do
group.avatar_url(only_path: false)
end
description: 'Avatar URL of the group'
field :custom_emoji, Types::CustomEmojiType.connection_type, null: true,
description: 'Custom emoji within this namespace',
@ -44,8 +41,7 @@ module Types
description: 'Indicates if a group is disabled from getting mentioned'
field :parent, GroupType, null: true,
description: 'Parent group',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, obj.parent_id).find }
description: 'Parent group'
field :issues,
Types::IssueType.connection_type,
@ -120,6 +116,14 @@ module Types
.execute
end
def avatar_url
object.avatar_url(only_path: false)
end
def parent
Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, object.parent_id).find
end
private
def group

View File

@ -132,8 +132,7 @@ module Types
description: 'Labels of the merge request'
field :discussion_locked, GraphQL::BOOLEAN_TYPE,
description: 'Indicates if comments on the merge request are locked to members only',
null: false,
resolve: -> (obj, _args, _ctx) { !!obj.discussion_locked }
null: false
field :time_estimate, GraphQL::INT_TYPE, null: false,
description: 'Time estimate of the merge request'
field :total_time_spent, GraphQL::INT_TYPE, null: false,
@ -200,6 +199,11 @@ module Types
def source_branch_protected
object.source_project.present? && ProtectedBranch.protected?(object.source_project, object.source_branch)
end
def discussion_locked
!!object.discussion_locked
end
end
end
Types::MergeRequestType.prepend_if_ee('::EE::Types::MergeRequestType')

View File

@ -21,6 +21,7 @@ module Types
field :description, GraphQL::STRING_TYPE, null: true,
description: 'Description of the namespace'
markdown_field :description_html, null: true
field :visibility, GraphQL::STRING_TYPE, null: true,
description: 'Visibility of the namespace'
field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true, method: :lfs_enabled?,
@ -30,12 +31,15 @@ module Types
field :root_storage_statistics, Types::RootStorageStatisticsType,
null: true,
description: 'Aggregated storage statistics of the namespace. Only available for root namespaces',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchRootStorageStatisticsLoader.new(obj.id).find }
description: 'Aggregated storage statistics of the namespace. Only available for root namespaces'
field :projects, Types::ProjectType.connection_type, null: false,
description: 'Projects within this namespace',
resolver: ::Resolvers::NamespaceProjectsResolver
def root_storage_statistics
Gitlab::Graphql::Loaders::BatchRootStorageStatisticsLoader.new(object.id).find
end
end
end

View File

@ -21,25 +21,43 @@ module Types
# Fields for text positions
field :old_line, GraphQL::INT_TYPE, null: true,
description: 'Line on start SHA that was changed',
resolve: -> (position, _args, _ctx) { position.old_line if position.on_text? }
description: 'Line on start SHA that was changed'
field :new_line, GraphQL::INT_TYPE, null: true,
description: 'Line on HEAD SHA that was changed',
resolve: -> (position, _args, _ctx) { position.new_line if position.on_text? }
description: 'Line on HEAD SHA that was changed'
# Fields for image positions
field :x, GraphQL::INT_TYPE, null: true,
description: 'X position of the note',
resolve: -> (position, _args, _ctx) { position.x if position.on_image? }
description: 'X position of the note'
field :y, GraphQL::INT_TYPE, null: true,
description: 'Y position of the note',
resolve: -> (position, _args, _ctx) { position.y if position.on_image? }
description: 'Y position of the note'
field :width, GraphQL::INT_TYPE, null: true,
description: 'Total width of the image',
resolve: -> (position, _args, _ctx) { position.width if position.on_image? }
description: 'Total width of the image'
field :height, GraphQL::INT_TYPE, null: true,
description: 'Total height of the image',
resolve: -> (position, _args, _ctx) { position.height if position.on_image? }
description: 'Total height of the image'
def old_line
object.old_line if object.on_text?
end
def new_line
object.new_line if object.on_text?
end
def x
object.x if object.on_image?
end
def y
object.y if object.on_image?
end
def width
object.width if object.on_image?
end
def height
object.height if object.on_image?
end
end
# rubocop: enable Graphql/AuthorizeTypes
end

View File

@ -16,13 +16,11 @@ module Types
field :project, Types::ProjectType,
null: true,
description: 'Project associated with the note',
resolve: -> (note, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, note.project_id).find }
description: 'Project associated with the note'
field :author, Types::UserType,
null: false,
description: 'User who wrote this note',
resolve: -> (note, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, note.author_id).find }
description: 'User who wrote this note'
field :system, GraphQL::BOOLEAN_TYPE,
null: false,
@ -52,6 +50,14 @@ module Types
def system_note_icon_name
SystemNoteHelper.system_note_icon_name(object) if object.system?
end
def project
Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find
end
def author
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find
end
end
end
end

View File

@ -19,7 +19,9 @@ module Types
permission_field field_name, method: :"can_#{field_name}?", calls_gitaly: true
end
permission_field :can_merge, calls_gitaly: true, resolve: -> (object, args, context) do
permission_field :can_merge, calls_gitaly: true
def can_merge
object.can_be_merged_by?(context[:current_user])
end
end

View File

@ -67,33 +67,25 @@ module Types
description: 'E-mail address of the service desk.'
field :avatar_url, GraphQL::STRING_TYPE, null: true, calls_gitaly: true,
description: 'URL to avatar image file of the project',
resolve: -> (project, args, ctx) do
project.avatar_url(only_path: false)
end
description: 'URL to avatar image file of the project'
%i[issues merge_requests wiki snippets].each do |feature|
field "#{feature}_enabled", GraphQL::BOOLEAN_TYPE, null: true,
description: "Indicates if #{feature.to_s.titleize.pluralize} are enabled for the current user",
resolve: -> (project, args, ctx) do
project.feature_available?(feature, ctx[:current_user])
end
description: "Indicates if #{feature.to_s.titleize.pluralize} are enabled for the current user"
define_method "#{feature}_enabled" do
object.feature_available?(feature, context[:current_user])
end
end
field :jobs_enabled, GraphQL::BOOLEAN_TYPE, null: true,
description: 'Indicates if CI/CD pipeline jobs are enabled for the current user',
resolve: -> (project, args, ctx) do
project.feature_available?(:builds, ctx[:current_user])
end
description: 'Indicates if CI/CD pipeline jobs are enabled for the current user'
field :public_jobs, GraphQL::BOOLEAN_TYPE, method: :public_builds, null: true,
description: 'Indicates if there is public access to pipelines and job details of the project, including output logs and artifacts'
field :open_issues_count, GraphQL::INT_TYPE, null: true,
description: 'Number of open issues for the project',
resolve: -> (project, args, ctx) do
project.open_issues_count if project.feature_available?(:issues, ctx[:current_user])
end
description: 'Number of open issues for the project'
field :import_status, GraphQL::STRING_TYPE, null: true,
description: 'Status of import background job of the project'
@ -123,8 +115,7 @@ module Types
field :statistics, Types::ProjectStatisticsType,
null: true,
description: 'Statistics of the project',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchProjectStatisticsLoader.new(obj.id).find }
description: 'Statistics of the project'
field :repository, Types::RepositoryType, null: true,
description: 'Git repository of the project'
@ -334,6 +325,22 @@ module Types
.execute
end
def avatar_url
object.avatar_url(only_path: false)
end
def jobs_enabled
object.feature_available?(:builds, context[:current_user])
end
def open_issues_count
object.open_issues_count if object.feature_available?(:issues, context[:current_user])
end
def statistics
Gitlab::Graphql::Loaders::BatchProjectStatisticsLoader.new(object.id).find
end
private
def project

View File

@ -24,7 +24,6 @@ module Types
field :current_user, Types::UserType,
null: true,
resolve: -> (_obj, _args, context) { context[:current_user] },
description: "Get information about current user"
field :namespace, Types::NamespaceType,
@ -116,6 +115,10 @@ module Types
id = ::Types::GlobalIDType[::ContainerRepository].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
def current_user
context[:current_user]
end
end
end

View File

@ -17,14 +17,12 @@ module Types
field :collapsed, GraphQL::BOOLEAN_TYPE,
description: 'Shows whether the blob should be displayed collapsed',
method: :collapsed?,
null: false,
resolve: -> (viewer, _args, _ctx) { !!viewer&.collapsed? }
null: false
field :too_large, GraphQL::BOOLEAN_TYPE,
description: 'Shows whether the blob too large to be displayed',
method: :too_large?,
null: false,
resolve: -> (viewer, _args, _ctx) { !!viewer&.too_large? }
null: false
field :render_error, GraphQL::STRING_TYPE,
description: 'Error rendering the blob content',
@ -38,6 +36,14 @@ module Types
field :loading_partial_name, GraphQL::STRING_TYPE,
description: 'Loading partial name',
null: false
def collapsed
!!object&.collapsed?
end
def too_large
!!object&.too_large?
end
end
end
end

View File

@ -20,8 +20,7 @@ module Types
field :locked_by_user, Types::UserType,
null: true,
authorize: :read_user,
description: 'The user currently holding a lock on the Terraform state',
resolve: -> (state, _, _) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, state.locked_by_user_id).find }
description: 'The user currently holding a lock on the Terraform state'
field :locked_at, Types::TimeType,
null: true,
@ -39,6 +38,10 @@ module Types
field :updated_at, Types::TimeType,
null: false,
description: 'Timestamp the Terraform state was updated'
def locked_by_user
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.locked_by_user_id).find
end
end
end
end

View File

@ -14,14 +14,12 @@ module Types
field :created_by_user, Types::UserType,
null: true,
authorize: :read_user,
description: 'The user that created this version',
resolve: -> (version, _, _) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, version.created_by_user_id).find }
description: 'The user that created this version'
field :job, Types::Ci::JobType,
null: true,
authorize: :read_build,
description: 'The job that created this version',
resolve: -> (version, _, _) { Gitlab::Graphql::Loaders::BatchModelLoader.new(::Ci::Build, version.ci_build_id).find }
description: 'The job that created this version'
field :created_at, Types::TimeType,
null: false,
@ -30,6 +28,14 @@ module Types
field :updated_at, Types::TimeType,
null: false,
description: 'Timestamp the version was updated'
def created_by_user
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.created_by_user_id).find
end
def job
Gitlab::Graphql::Loaders::BatchModelLoader.new(::Ci::Build, object.ci_build_id).find
end
end
end
end

View File

@ -16,19 +16,16 @@ module Types
field :project, Types::ProjectType,
description: 'The project this todo is associated with',
null: true,
authorize: :read_project,
resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, todo.project_id).find }
authorize: :read_project
field :group, Types::GroupType,
description: 'Group this todo is associated with',
null: true,
authorize: :read_group,
resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, todo.group_id).find }
authorize: :read_group
field :author, Types::UserType,
description: 'The author of this todo',
null: false,
resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, todo.author_id).find }
null: false
field :action, Types::TodoActionEnum,
description: 'Action of the todo',
@ -50,5 +47,17 @@ module Types
field :created_at, Types::TimeType,
description: 'Timestamp this todo was created',
null: false
def project
Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find
end
def group
Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, object.group_id).find
end
def author
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find
end
end
end

View File

@ -15,13 +15,14 @@ module Types
field :web_path, GraphQL::STRING_TYPE, null: true,
description: 'Web path of the blob'
field :lfs_oid, GraphQL::STRING_TYPE, null: true,
description: 'LFS ID of the blob',
resolve: -> (blob, args, ctx) do
Gitlab::Graphql::Loaders::BatchLfsOidLoader.new(blob.repository, blob.id).find
end
description: 'LFS ID of the blob'
field :mode, GraphQL::STRING_TYPE, null: true,
description: 'Blob mode in numeric format'
# rubocop: enable Graphql/AuthorizeTypes
def lfs_oid
Gitlab::Graphql::Loaders::BatchLfsOidLoader.new(object.repository, object.id).find
end
end
# rubocop: enable Graphql/AuthorizeTypes
end
end

View File

@ -8,27 +8,32 @@ module Types
# Complexity 10 as it triggers a Gitaly call on each render
field :last_commit, Types::CommitType,
null: true, complexity: 10, calls_gitaly: true, resolver: Resolvers::LastCommitResolver,
description: 'Last commit for the tree'
null: true, complexity: 10, calls_gitaly: true, resolver: Resolvers::LastCommitResolver,
description: 'Last commit for the tree'
field :trees, Types::Tree::TreeEntryType.connection_type, null: false,
description: 'Trees of the tree',
resolve: -> (obj, args, ctx) do
Gitlab::Graphql::Representation::TreeEntry.decorate(obj.trees, obj.repository)
end
description: 'Trees of the tree'
field :submodules, Types::Tree::SubmoduleType.connection_type, null: false,
description: 'Sub-modules of the tree',
calls_gitaly: true, resolve: -> (obj, args, ctx) do
Gitlab::Graphql::Representation::SubmoduleTreeEntry.decorate(obj.submodules, obj)
end
calls_gitaly: true
field :blobs, Types::Tree::BlobType.connection_type, null: false,
description: 'Blobs of the tree',
calls_gitaly: true, resolve: -> (obj, args, ctx) do
Gitlab::Graphql::Representation::TreeEntry.decorate(obj.blobs, obj.repository)
end
# rubocop: enable Graphql/AuthorizeTypes
calls_gitaly: true
def trees
Gitlab::Graphql::Representation::TreeEntry.decorate(object.trees, object.repository)
end
def submodules
Gitlab::Graphql::Representation::SubmoduleTreeEntry.decorate(object.submodules, object)
end
def blobs
Gitlab::Graphql::Representation::TreeEntry.decorate(object.blobs, object.repository)
end
end
# rubocop: enable Graphql/AuthorizeTypes
end
end

View File

@ -1,6 +0,0 @@
# frozen_string_literal: true
module Analytics::DevopsAdoption
def self.table_name_prefix
'analytics_devops_adoption_'
end
end

View File

@ -1,24 +0,0 @@
# frozen_string_literal: true
class Analytics::DevopsAdoption::Segment < ApplicationRecord
ALLOWED_SEGMENT_COUNT = 20
has_many :segment_selections
has_many :groups, through: :segment_selections
validates :name, presence: true, uniqueness: true, length: { maximum: 255 }
validate :validate_segment_count, on: :create
accepts_nested_attributes_for :segment_selections, allow_destroy: true
scope :ordered_by_name, -> { order(:name) }
scope :with_groups, -> { preload(:groups) }
private
def validate_segment_count
if self.class.count >= ALLOWED_SEGMENT_COUNT
errors.add(:name, s_('DevopsAdoptionSegment|The maximum number of segments has been reached'))
end
end
end

View File

@ -1,36 +0,0 @@
# frozen_string_literal: true
class Analytics::DevopsAdoption::SegmentSelection < ApplicationRecord
ALLOWED_SELECTIONS_PER_SEGMENT = 20
belongs_to :segment
belongs_to :project
belongs_to :group
validates :segment, presence: true
validates :project, presence: { unless: :group }
validates :project_id, uniqueness: { scope: :segment_id, if: :project }
validates :group, presence: { unless: :project }
validates :group_id, uniqueness: { scope: :segment_id, if: :group }
validate :exclusive_project_or_group
validate :validate_selection_count, on: :create
private
def exclusive_project_or_group
if project.present? && group.present?
errors.add(:group, s_('DevopsAdoptionSegmentSelection|The selection cannot be configured for a project and for a group at the same time'))
end
end
def validate_selection_count
return unless segment
# handle single model creation and bulk creation from accepts_nested_attributes_for
selections = segment.segment_selections + [self]
if selections.reject(&:marked_for_destruction?).uniq.size > ALLOWED_SELECTIONS_PER_SEGMENT
errors.add(:segment, s_('DevopsAdoptionSegmentSelection|The maximum number of selections has been reached'))
end
end
end

View File

@ -31,9 +31,8 @@ module Import
project_name,
target_namespace,
current_user,
access_params,
type: provider
).execute(extra_project_attrs)
type: provider,
**access_params).execute(extra_project_attrs)
end
def repo

View File

@ -32,7 +32,7 @@
%button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
%span.dropdown-toggle-text
= _('Select project')
= icon('chevron-down')
= sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
%span.form-text.text-muted &nbsp;
.form-group
@ -43,7 +43,7 @@
%button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
%span.dropdown-toggle-text
= _('Select project to choose zone')
= icon('chevron-down')
= sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
%p.form-text.text-muted
= s_('ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: zones_link_url }, help_link_end: help_link_end }
@ -59,7 +59,7 @@
%button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
%span.dropdown-toggle-text
= _('Select project and zone to choose machine type')
= icon('chevron-down')
= sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
%p.form-text.text-muted
= s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end }

View File

@ -9,6 +9,7 @@
= javascript_include_tag 'https://connect-cdn.atl-paas.net/all.js'
= javascript_include_tag 'https://unpkg.com/jquery@3.3.1/dist/jquery.min.js'
= Gon::Base.render_data(nonce: content_security_policy_nonce)
= yield :head
%body
.ac-content

View File

@ -3,7 +3,6 @@
- if issuable.relocation_target
- page_canonical_link issuable.relocation_target.present(current_user: current_user).web_url
= render_if_exists "projects/issues/alert_blocked", issue: issuable, current_user: current_user
= render "projects/issues/alert_moved_from_service_desk", issue: issuable
= render 'shared/issue_type/details_header', issuable: issuable

View File

@ -1,56 +0,0 @@
#modal_merge_info.modal{ tabindex: '-1' }
.modal-dialog.modal-lg
.modal-content
.modal-header
%h3.modal-title Check out, review, and merge locally
%button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
%span{ "aria-hidden": true } &times;
.modal-body
%p
%strong Step 1.
Fetch and check out the branch for this merge request
= clipboard_button(target: "pre#merge-info-1", title: _("Copy commands"))
%pre.dark#merge-info-1
- if @merge_request.for_fork?
-# All repo/branch refs have been quoted to allow support for special characters (such as #my-branch)
:preserve
git fetch "#{h default_url_to_repo(@merge_request.source_project)}" "#{h @merge_request.source_branch}"
git checkout -b "#{h @merge_request.source_project_path}-#{h @merge_request.source_branch}" FETCH_HEAD
- else
:preserve
git fetch origin
git checkout -b "#{h @merge_request.source_branch}" "origin/#{h @merge_request.source_branch}"
%p
%strong Step 2.
Review the changes locally
%p
%strong Step 3.
Merge the branch and fix any conflicts that come up
= clipboard_button(target: "pre#merge-info-3", title: _("Copy commands"))
%pre.dark#merge-info-3
- if @merge_request.for_fork?
:preserve
git fetch origin
git checkout "#{h @merge_request.target_branch}"
git merge --no-ff "#{h @merge_request.source_project_path}-#{h @merge_request.source_branch}"
- else
:preserve
git fetch origin
git checkout "#{h @merge_request.target_branch}"
git merge --no-ff "#{h @merge_request.source_branch}"
%p
%strong Step 4.
Push the result of the merge to GitLab
= clipboard_button(target: "pre#merge-info-4", title: _("Copy commands"))
%pre.dark#merge-info-4
:preserve
git push origin "#{h @merge_request.target_branch}"
- unless @merge_request.can_be_merged_by?(current_user)
%p
Note that pushing to GitLab requires write access to this repository.
%p
%strong Tip:
= succeed '.' do
You can also checkout merge requests locally by
= link_to 'following these guidelines', help_page_path('user/project/merge_requests/reviewing_and_managing_merge_requests.md', anchor: "checkout-merge-requests-locally-through-the-head-ref"), target: '_blank', rel: 'noopener noreferrer'

View File

@ -16,9 +16,6 @@
.merge-request{ data: { mr_action: mr_action, url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project), lock_version: @merge_request.lock_version } }
= render "projects/merge_requests/mr_title"
- if @merge_request.source_branch_exists?
= render "projects/merge_requests/how_to_merge"
.merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
= render "projects/merge_requests/mr_box"
.merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }

View File

@ -88,3 +88,6 @@
= form.hidden_field :issue_type
= form.hidden_field :lock_version
- if @vulnerability_id
= hidden_field_tag 'vulnerability_id', @vulnerability_id

View File

@ -18,7 +18,7 @@
%h4.gl-flex-grow-1
= Feature.enabled?(:security_auto_fix) && @user.bot? ? s_('UserProfile|Bot activity') : s_('UserProfile|Activity')
= link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all"
.overview-content-list{ data: { href: user_path } }
.overview-content-list{ data: { href: user_activity_path } }
.center.light.loading
.spinner.spinner-md

View File

@ -139,7 +139,7 @@
- if can?(current_user, :read_cross_project)
%h4.prepend-top-20
= s_('UserProfile|Most Recent Activity')
.content_list{ data: { href: user_path } }
.content_list{ data: { href: user_activity_path } }
.loading
.spinner.spinner-md
- unless @user.bot?

View File

@ -1852,7 +1852,7 @@
:urgency: :high
:resource_boundary: :unknown
:weight: 1
:idempotent:
:idempotent: true
:tags: []
- :name: project_daily_statistics
:feature_category: :source_code_management

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
# Worker for updating any project specific caches.
class ProjectCacheWorker # rubocop:disable Scalability/IdempotentWorker
class ProjectCacheWorker
include ApplicationWorker
LEASE_TIMEOUT = 15.minutes.to_i
@ -9,6 +9,7 @@ class ProjectCacheWorker # rubocop:disable Scalability/IdempotentWorker
feature_category :source_code_management
urgency :high
loggable_arguments 1, 2, 3
idempotent!
# project_id - The ID of the project for which to flush the cache.
# files - An Array containing extra types of files to refresh such as

View File

@ -0,0 +1,5 @@
---
title: Add support for filtering direct group members by 2FA enabled/disabled
merge_request: 48084
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Replace how to merge HAML with Vue component
merge_request: 48766
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Replace fa-chevron-down icons with GitLab SVG in gcp cluster form
merge_request: 48656
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Move users#show.json to users#activity.json
merge_request: 48712
author: Takuya Noguchi
type: other

View File

@ -0,0 +1,5 @@
---
title: Add analytics_devops_adoption_snapshots table
merge_request: 47388
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Add loading state to assignees header
merge_request: 48392
author:
type: added

View File

@ -1,5 +1,5 @@
---
title: Add loading state to assignees dropdown
title: Add loading state to boards assignees header dropdown
merge_request: 47848
author:
type: added

View File

@ -38,7 +38,6 @@
- dependency_proxy
- dependency_scanning
- design_management
- design_system
- devops_reports
- disaster_recovery
- dynamic_application_security_testing
@ -54,7 +53,6 @@
- git_lfs
- gitaly
- gitlab_docs
- gitlab_handbook
- global_search
- helm_chart_registry
- importers
@ -69,7 +67,6 @@
- issue_tracking
- jenkins_importer
- jira_importer
- jupyter_notebooks
- kubernetes_management
- license_compliance
- live_preview

View File

@ -1,60 +1,47 @@
concern :gitactionable do
scope(controller: :git_http) do
get '/info/refs', action: :info_refs
post '/git-upload-pack', action: :git_upload_pack
post '/git-receive-pack', action: :git_receive_pack
end
end
concern :lfsable do
# Git LFS API (metadata)
scope(path: 'info/lfs/objects', controller: :lfs_api) do
post :batch
post '/', action: :deprecated
get '/*oid', action: :deprecated
end
scope(path: 'info/lfs') do
resources :lfs_locks, controller: :lfs_locks_api, path: 'locks' do
post :unlock, on: :member
post :verify, on: :collection
end
end
# GitLab LFS object storage
scope(path: 'gitlab-lfs/objects/*oid', controller: :lfs_storage, constraints: { oid: /[a-f0-9]{64}/ }) do
get '/', action: :download
scope constraints: { size: /[0-9]+/ } do
put '/*size/authorize', action: :upload_authorize
put '/*size', action: :upload_finalize
end
end
end
# Git route for personal and project snippets
scope(path: ':namespace_id/:repository_id',
format: nil,
constraints: { namespace_id: Gitlab::PathRegex.personal_and_project_snippets_path_regex, repository_id: /\d+\.git/ },
module: :repositories) do
concerns :gitactionable
end
scope(path: '*namespace_id/:repository_id',
format: nil,
constraints: { namespace_id: Gitlab::PathRegex.full_namespace_route_regex }) do
scope(constraints: { repository_id: Gitlab::PathRegex.project_git_route_regex }) do
scope(path: '*repository_path', format: false) do
constraints(repository_path: Gitlab::PathRegex.repository_git_route_regex) do
scope(module: :repositories) do
concerns :gitactionable
concerns :lfsable
# Git HTTP API
scope(controller: :git_http) do
get '/info/refs', action: :info_refs
post '/git-upload-pack', action: :git_upload_pack
post '/git-receive-pack', action: :git_receive_pack
end
# NOTE: LFS routes are exposed on all repository types, but we still check for
# LFS availability on the repository container in LfsRequest#require_lfs_enabled!
# Git LFS API (metadata)
scope(path: 'info/lfs/objects', controller: :lfs_api) do
post :batch
post '/', action: :deprecated
get '/*oid', action: :deprecated
end
scope(path: 'info/lfs') do
resources :lfs_locks, controller: :lfs_locks_api, path: 'locks' do
post :unlock, on: :member
post :verify, on: :collection
end
end
# GitLab LFS object storage
scope(path: 'gitlab-lfs/objects/*oid', controller: :lfs_storage, constraints: { oid: /[a-f0-9]{64}/ }) do
get '/', action: :download
constraints(size: /[0-9]+/) do
put '/*size/authorize', action: :upload_authorize
put '/*size', action: :upload_finalize
end
end
end
end
# Redirect /group/project.wiki.git to the project wiki
scope(format: true, constraints: { repository_id: Gitlab::PathRegex.project_wiki_git_route_regex, format: :git }) do
constraints(repository_path: Gitlab::PathRegex.repository_wiki_git_route_regex) do
wiki_redirect = redirect do |params, request|
project_id = params[:repository_id].delete_suffix('.wiki')
path = [params[:namespace_id], project_id, 'wikis'].join('/')
container_path = params[:repository_path].delete_suffix('.wiki.git')
path = File.join(container_path, '-', 'wikis')
path << "?#{request.query_string}" unless request.query_string.blank?
path
end
@ -63,22 +50,14 @@ scope(path: '*namespace_id/:repository_id',
end
# Redirect /group/project/info/refs to /group/project.git/info/refs
scope(constraints: { repository_id: Gitlab::PathRegex.project_route_regex }) do
# Allow /info/refs, /info/refs?service=git-upload-pack, and
# /info/refs?service=git-receive-pack, but nothing else.
#
git_http_handshake = lambda do |request|
::Constraints::ProjectUrlConstrainer.new.matches?(request, existence_check: false) &&
(request.query_string.blank? ||
request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/))
end
# This allows cloning a repository without the trailing `.git`
constraints(repository_path: Gitlab::PathRegex.repository_route_regex) do
ref_redirect = redirect do |params, request|
path = "#{params[:namespace_id]}/#{params[:repository_id]}.git/info/refs"
path = "#{params[:repository_path]}.git/info/refs"
path << "?#{request.query_string}" unless request.query_string.blank?
path
end
get '/info/refs', constraints: git_http_handshake, to: ref_redirect
get '/info/refs', constraints: ::Constraints::RepositoryRedirectUrlConstrainer.new, to: ref_redirect
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class CreateAnalyticsDevopsAdoptionSnapshots < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
create_table :analytics_devops_adoption_snapshots do |t|
t.references :segment, index: false, null: false, foreign_key: { to_table: :analytics_devops_adoption_segments, on_delete: :cascade }
t.datetime_with_timezone :recorded_at, null: false
t.boolean :issue_opened, null: false
t.boolean :merge_request_opened, null: false
t.boolean :merge_request_approved, null: false
t.boolean :runner_configured, null: false
t.boolean :pipeline_succeeded, null: false
t.boolean :deploy_succeeded, null: false
t.boolean :security_scan_succeeded, null: false
t.index [:segment_id, :recorded_at], name: 'index_on_snapshots_segment_id_recorded_at'
end
end
end

View File

@ -0,0 +1 @@
7a905f8e636be21e328a622d9871018903982989836e6e0def09fd2c2826691f

View File

@ -8990,6 +8990,28 @@ CREATE SEQUENCE analytics_devops_adoption_segments_id_seq
ALTER SEQUENCE analytics_devops_adoption_segments_id_seq OWNED BY analytics_devops_adoption_segments.id;
CREATE TABLE analytics_devops_adoption_snapshots (
id bigint NOT NULL,
segment_id bigint NOT NULL,
recorded_at timestamp with time zone NOT NULL,
issue_opened boolean NOT NULL,
merge_request_opened boolean NOT NULL,
merge_request_approved boolean NOT NULL,
runner_configured boolean NOT NULL,
pipeline_succeeded boolean NOT NULL,
deploy_succeeded boolean NOT NULL,
security_scan_succeeded boolean NOT NULL
);
CREATE SEQUENCE analytics_devops_adoption_snapshots_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE analytics_devops_adoption_snapshots_id_seq OWNED BY analytics_devops_adoption_snapshots.id;
CREATE TABLE analytics_instance_statistics_measurements (
id bigint NOT NULL,
count bigint NOT NULL,
@ -17773,6 +17795,8 @@ ALTER TABLE ONLY analytics_devops_adoption_segment_selections ALTER COLUMN id SE
ALTER TABLE ONLY analytics_devops_adoption_segments ALTER COLUMN id SET DEFAULT nextval('analytics_devops_adoption_segments_id_seq'::regclass);
ALTER TABLE ONLY analytics_devops_adoption_snapshots ALTER COLUMN id SET DEFAULT nextval('analytics_devops_adoption_snapshots_id_seq'::regclass);
ALTER TABLE ONLY analytics_instance_statistics_measurements ALTER COLUMN id SET DEFAULT nextval('analytics_instance_statistics_measurements_id_seq'::regclass);
ALTER TABLE ONLY appearances ALTER COLUMN id SET DEFAULT nextval('appearances_id_seq'::regclass);
@ -18754,6 +18778,9 @@ ALTER TABLE ONLY analytics_devops_adoption_segment_selections
ALTER TABLE ONLY analytics_devops_adoption_segments
ADD CONSTRAINT analytics_devops_adoption_segments_pkey PRIMARY KEY (id);
ALTER TABLE ONLY analytics_devops_adoption_snapshots
ADD CONSTRAINT analytics_devops_adoption_snapshots_pkey PRIMARY KEY (id);
ALTER TABLE ONLY analytics_instance_statistics_measurements
ADD CONSTRAINT analytics_instance_statistics_measurements_pkey PRIMARY KEY (id);
@ -21632,6 +21659,8 @@ CREATE UNIQUE INDEX index_on_segment_selections_project_id_segment_id ON analyti
CREATE INDEX index_on_segment_selections_segment_id ON analytics_devops_adoption_segment_selections USING btree (segment_id);
CREATE INDEX index_on_snapshots_segment_id_recorded_at ON analytics_devops_adoption_snapshots USING btree (segment_id, recorded_at);
CREATE INDEX index_on_users_lower_email ON users USING btree (lower((email)::text));
CREATE INDEX index_on_users_lower_username ON users USING btree (lower((username)::text));
@ -23742,6 +23771,9 @@ ALTER TABLE ONLY saml_group_links
ALTER TABLE ONLY group_custom_attributes
ADD CONSTRAINT fk_rails_246e0db83a FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY analytics_devops_adoption_snapshots
ADD CONSTRAINT fk_rails_25da9a92c0 FOREIGN KEY (segment_id) REFERENCES analytics_devops_adoption_segments(id) ON DELETE CASCADE;
ALTER TABLE ONLY cluster_agents
ADD CONSTRAINT fk_rails_25e9fc2d5d FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;

View File

@ -5298,12 +5298,12 @@ Represents a DAST Site Validation
"""
type DastSiteValidation {
"""
ID of the site validation
Global ID of the site validation
"""
id: DastSiteValidationID!
"""
The status of the validation
Status of the site validation
"""
status: DastSiteProfileValidationStatusEnum!
}
@ -6539,33 +6539,18 @@ type DevopsAdoptionSegment {
"""
Assigned groups
"""
groups(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): GroupConnection
groups: [Group!]
"""
ID of the segment
"""
id: ID!
"""
The latest adoption metrics for the segment
"""
latestSnapshot: DevopsAdoptionSnapshot
"""
Name of the segment
"""
@ -6607,6 +6592,61 @@ type DevopsAdoptionSegmentEdge {
node: DevopsAdoptionSegment
}
"""
Snapshot
"""
type DevopsAdoptionSnapshot {
"""
At least one deployment succeeded
"""
deploySucceeded: Boolean!
"""
The end time for the snapshot where the data points were collected
"""
endTime: Time!
"""
At least one issue was opened
"""
issueOpened: Boolean!
"""
At least one merge request was approved
"""
mergeRequestApproved: Boolean!
"""
At least one merge request was opened
"""
mergeRequestOpened: Boolean!
"""
At least one pipeline succeeded
"""
pipelineSucceeded: Boolean!
"""
The time the snapshot was recorded
"""
recordedAt: Time!
"""
At least one runner was used
"""
runnerConfigured: Boolean!
"""
At least one security scan succeeded
"""
securityScanSucceeded: Boolean!
"""
The start time for the snapshot where the data points were collected
"""
startTime: Time!
}
input DiffImagePositionInput {
"""
Merge base of the branch the comment was made on
@ -7803,12 +7843,12 @@ type EpicIssue implements CurrentUserTodos & Noteable {
author: User!
"""
Indicates the issue is blocked
Indicates the issue is blocked.
"""
blocked: Boolean!
"""
Count of issues blocking this issue
Count of issues blocking this issue.
"""
blockedByCount: Int
@ -7918,7 +7958,7 @@ type EpicIssue implements CurrentUserTodos & Noteable {
emailsDisabled: Boolean!
"""
Epic to which this issue belongs
Epic to which this issue belongs.
"""
epic: Epic
@ -7953,7 +7993,7 @@ type EpicIssue implements CurrentUserTodos & Noteable {
iid: ID!
"""
Iteration of the issue
Iteration of the issue.
"""
iteration: Iteration
@ -8088,7 +8128,7 @@ type EpicIssue implements CurrentUserTodos & Noteable {
state: IssueState!
"""
Indicates whether an issue is published to the status page
Indicates whether an issue is published to the status page.
"""
statusPagePublishedIncident: Boolean
@ -8168,7 +8208,7 @@ type EpicIssue implements CurrentUserTodos & Noteable {
webUrl: String!
"""
Weight of the issue
Weight of the issue.
"""
weight: Int
}
@ -9872,41 +9912,6 @@ type Group {
webUrl: String!
}
"""
The connection type for Group.
"""
type GroupConnection {
"""
A list of edges.
"""
edges: [GroupEdge]
"""
A list of nodes.
"""
nodes: [Group]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type GroupEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: Group
}
"""
Identifier of Group
"""
@ -10484,12 +10489,12 @@ type Issue implements CurrentUserTodos & Noteable {
author: User!
"""
Indicates the issue is blocked
Indicates the issue is blocked.
"""
blocked: Boolean!
"""
Count of issues blocking this issue
Count of issues blocking this issue.
"""
blockedByCount: Int
@ -10599,7 +10604,7 @@ type Issue implements CurrentUserTodos & Noteable {
emailsDisabled: Boolean!
"""
Epic to which this issue belongs
Epic to which this issue belongs.
"""
epic: Epic
@ -10629,7 +10634,7 @@ type Issue implements CurrentUserTodos & Noteable {
iid: ID!
"""
Iteration of the issue
Iteration of the issue.
"""
iteration: Iteration
@ -10759,7 +10764,7 @@ type Issue implements CurrentUserTodos & Noteable {
state: IssueState!
"""
Indicates whether an issue is published to the status page
Indicates whether an issue is published to the status page.
"""
statusPagePublishedIncident: Boolean
@ -10839,7 +10844,7 @@ type Issue implements CurrentUserTodos & Noteable {
webUrl: String!
"""
Weight of the issue
Weight of the issue.
"""
weight: Int
}

View File

@ -14507,7 +14507,7 @@
"fields": [
{
"name": "id",
"description": "ID of the site validation",
"description": "Global ID of the site validation",
"args": [
],
@ -14525,7 +14525,7 @@
},
{
"name": "status",
"description": "The status of the validation",
"description": "Status of the site validation",
"args": [
],
@ -18042,51 +18042,20 @@
"name": "groups",
"description": "Assigned groups",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "GroupConnection",
"ofType": null
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Group",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
@ -18109,6 +18078,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "latestSnapshot",
"description": "The latest adoption metrics for the segment",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "DevopsAdoptionSnapshot",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "Name of the segment",
@ -18247,6 +18230,199 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DevopsAdoptionSnapshot",
"description": "Snapshot",
"fields": [
{
"name": "deploySucceeded",
"description": "At least one deployment succeeded",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "endTime",
"description": "The end time for the snapshot where the data points were collected",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "issueOpened",
"description": "At least one issue was opened",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mergeRequestApproved",
"description": "At least one merge request was approved",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mergeRequestOpened",
"description": "At least one merge request was opened",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pipelineSucceeded",
"description": "At least one pipeline succeeded",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "recordedAt",
"description": "The time the snapshot was recorded",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "runnerConfigured",
"description": "At least one runner was used",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "securityScanSucceeded",
"description": "At least one security scan succeeded",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "startTime",
"description": "The start time for the snapshot where the data points were collected",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "DiffImagePositionInput",
@ -21660,7 +21836,7 @@
},
{
"name": "blocked",
"description": "Indicates the issue is blocked",
"description": "Indicates the issue is blocked.",
"args": [
],
@ -21678,7 +21854,7 @@
},
{
"name": "blockedByCount",
"description": "Count of issues blocking this issue",
"description": "Count of issues blocking this issue.",
"args": [
],
@ -21976,7 +22152,7 @@
},
{
"name": "epic",
"description": "Epic to which this issue belongs",
"description": "Epic to which this issue belongs.",
"args": [
],
@ -22082,7 +22258,7 @@
},
{
"name": "iteration",
"description": "Iteration of the issue",
"description": "Iteration of the issue.",
"args": [
],
@ -22424,7 +22600,7 @@
},
{
"name": "statusPagePublishedIncident",
"description": "Indicates whether an issue is published to the status page",
"description": "Indicates whether an issue is published to the status page.",
"args": [
],
@ -22696,7 +22872,7 @@
},
{
"name": "weight",
"description": "Weight of the issue",
"description": "Weight of the issue.",
"args": [
],
@ -26953,118 +27129,6 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "GroupConnection",
"description": "The connection type for Group.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "GroupEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Group",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "GroupEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Group",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "GroupID",
@ -28744,7 +28808,7 @@
},
{
"name": "blocked",
"description": "Indicates the issue is blocked",
"description": "Indicates the issue is blocked.",
"args": [
],
@ -28762,7 +28826,7 @@
},
{
"name": "blockedByCount",
"description": "Count of issues blocking this issue",
"description": "Count of issues blocking this issue.",
"args": [
],
@ -29060,7 +29124,7 @@
},
{
"name": "epic",
"description": "Epic to which this issue belongs",
"description": "Epic to which this issue belongs.",
"args": [
],
@ -29152,7 +29216,7 @@
},
{
"name": "iteration",
"description": "Iteration of the issue",
"description": "Iteration of the issue.",
"args": [
],
@ -29480,7 +29544,7 @@
},
{
"name": "statusPagePublishedIncident",
"description": "Indicates whether an issue is published to the status page",
"description": "Indicates whether an issue is published to the status page.",
"args": [
],
@ -29752,7 +29816,7 @@
},
{
"name": "weight",
"description": "Weight of the issue",
"description": "Weight of the issue.",
"args": [
],

View File

@ -887,8 +887,8 @@ Represents a DAST Site Validation.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `id` | DastSiteValidationID! | ID of the site validation |
| `status` | DastSiteProfileValidationStatusEnum! | The status of the validation |
| `id` | DastSiteValidationID! | Global ID of the site validation |
| `status` | DastSiteProfileValidationStatusEnum! | Status of the site validation |
### DastSiteValidationCreatePayload
@ -1104,10 +1104,28 @@ Segment.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `groups` | GroupConnection | Assigned groups |
| `groups` | Group! => Array | Assigned groups |
| `id` | ID! | ID of the segment |
| `latestSnapshot` | DevopsAdoptionSnapshot | The latest adoption metrics for the segment |
| `name` | String! | Name of the segment |
### DevopsAdoptionSnapshot
Snapshot.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `deploySucceeded` | Boolean! | At least one deployment succeeded |
| `endTime` | Time! | The end time for the snapshot where the data points were collected |
| `issueOpened` | Boolean! | At least one issue was opened |
| `mergeRequestApproved` | Boolean! | At least one merge request was approved |
| `mergeRequestOpened` | Boolean! | At least one merge request was opened |
| `pipelineSucceeded` | Boolean! | At least one pipeline succeeded |
| `recordedAt` | Time! | The time the snapshot was recorded |
| `runnerConfigured` | Boolean! | At least one runner was used |
| `securityScanSucceeded` | Boolean! | At least one security scan succeeded |
| `startTime` | Time! | The start time for the snapshot where the data points were collected |
### DiffPosition
| Field | Type | Description |
@ -1309,8 +1327,8 @@ Relationship between an epic and an issue.
| `alertManagementAlert` | AlertManagementAlert | Alert associated to this issue |
| `assignees` | UserConnection | Assignees of the issue |
| `author` | User! | User that created the issue |
| `blocked` | Boolean! | Indicates the issue is blocked |
| `blockedByCount` | Int | Count of issues blocking this issue |
| `blocked` | Boolean! | Indicates the issue is blocked. |
| `blockedByCount` | Int | Count of issues blocking this issue. |
| `closedAt` | Time | Timestamp of when the issue was closed |
| `confidential` | Boolean! | Indicates the issue is confidential |
| `createdAt` | Time! | Timestamp of when the issue was created |
@ -1323,14 +1341,14 @@ Relationship between an epic and an issue.
| `downvotes` | Int! | Number of downvotes the issue has received |
| `dueDate` | Time | Due date of the issue |
| `emailsDisabled` | Boolean! | Indicates if a project has email notifications disabled: `true` if email notifications are disabled |
| `epic` | Epic | Epic to which this issue belongs |
| `epic` | Epic | Epic to which this issue belongs. |
| `epicIssueId` | ID! | ID of the epic-issue relation |
| `healthStatus` | HealthStatus | Current health status. Returns null if `save_issuable_health_status` feature flag is disabled. |
| `humanTimeEstimate` | String | Human-readable time estimate of the issue |
| `humanTotalTimeSpent` | String | Human-readable total time reported as spent on the issue |
| `id` | ID | Global ID of the epic-issue relation |
| `iid` | ID! | Internal ID of the issue |
| `iteration` | Iteration | Iteration of the issue |
| `iteration` | Iteration | Iteration of the issue. |
| `labels` | LabelConnection | Labels of the issue |
| `metricImages` | MetricImage! => Array | Metric images associated to the issue. |
| `milestone` | Milestone | Milestone of the issue |
@ -1344,7 +1362,7 @@ Relationship between an epic and an issue.
| `severity` | IssuableSeverity | Severity level of the incident |
| `slaDueAt` | Time | Timestamp of when the issue SLA expires. |
| `state` | IssueState! | State of the issue |
| `statusPagePublishedIncident` | Boolean | Indicates whether an issue is published to the status page |
| `statusPagePublishedIncident` | Boolean | Indicates whether an issue is published to the status page. |
| `subscribed` | Boolean! | Indicates the currently logged in user is subscribed to the issue |
| `taskCompletionStatus` | TaskCompletionStatus! | Task completion status of the issue |
| `timeEstimate` | Int! | Time estimate of the issue |
@ -1360,7 +1378,7 @@ Relationship between an epic and an issue.
| `userPermissions` | IssuePermissions! | Permissions for the current user on the resource |
| `webPath` | String! | Web path of the issue |
| `webUrl` | String! | Web URL of the issue |
| `weight` | Int | Weight of the issue |
| `weight` | Int | Weight of the issue. |
### EpicPermissions
@ -1608,8 +1626,8 @@ Represents a recorded measurement (object count) for the Admins.
| `alertManagementAlert` | AlertManagementAlert | Alert associated to this issue |
| `assignees` | UserConnection | Assignees of the issue |
| `author` | User! | User that created the issue |
| `blocked` | Boolean! | Indicates the issue is blocked |
| `blockedByCount` | Int | Count of issues blocking this issue |
| `blocked` | Boolean! | Indicates the issue is blocked. |
| `blockedByCount` | Int | Count of issues blocking this issue. |
| `closedAt` | Time | Timestamp of when the issue was closed |
| `confidential` | Boolean! | Indicates the issue is confidential |
| `createdAt` | Time! | Timestamp of when the issue was created |
@ -1622,13 +1640,13 @@ Represents a recorded measurement (object count) for the Admins.
| `downvotes` | Int! | Number of downvotes the issue has received |
| `dueDate` | Time | Due date of the issue |
| `emailsDisabled` | Boolean! | Indicates if a project has email notifications disabled: `true` if email notifications are disabled |
| `epic` | Epic | Epic to which this issue belongs |
| `epic` | Epic | Epic to which this issue belongs. |
| `healthStatus` | HealthStatus | Current health status. Returns null if `save_issuable_health_status` feature flag is disabled. |
| `humanTimeEstimate` | String | Human-readable time estimate of the issue |
| `humanTotalTimeSpent` | String | Human-readable total time reported as spent on the issue |
| `id` | ID! | ID of the issue |
| `iid` | ID! | Internal ID of the issue |
| `iteration` | Iteration | Iteration of the issue |
| `iteration` | Iteration | Iteration of the issue. |
| `labels` | LabelConnection | Labels of the issue |
| `metricImages` | MetricImage! => Array | Metric images associated to the issue. |
| `milestone` | Milestone | Milestone of the issue |
@ -1641,7 +1659,7 @@ Represents a recorded measurement (object count) for the Admins.
| `severity` | IssuableSeverity | Severity level of the incident |
| `slaDueAt` | Time | Timestamp of when the issue SLA expires. |
| `state` | IssueState! | State of the issue |
| `statusPagePublishedIncident` | Boolean | Indicates whether an issue is published to the status page |
| `statusPagePublishedIncident` | Boolean | Indicates whether an issue is published to the status page. |
| `subscribed` | Boolean! | Indicates the currently logged in user is subscribed to the issue |
| `taskCompletionStatus` | TaskCompletionStatus! | Task completion status of the issue |
| `timeEstimate` | Int! | Time estimate of the issue |
@ -1657,7 +1675,7 @@ Represents a recorded measurement (object count) for the Admins.
| `userPermissions` | IssuePermissions! | Permissions for the current user on the resource |
| `webPath` | String! | Web path of the issue |
| `webUrl` | String! | Web URL of the issue |
| `weight` | Int | Weight of the issue |
| `weight` | Int | Weight of the issue. |
### IssueMoveListPayload

View File

@ -91,12 +91,22 @@ There are two ways to define the URL to be scanned by DAST:
1. Set the `DAST_WEBSITE` [variable](../../../ci/yaml/README.md#variables).
1. Add it in an `environment_url.txt` file at the root of your project.
This is great for testing in dynamic environments. In order to run DAST against
an app dynamically created during a GitLab CI/CD pipeline, have the app
persist its domain in an `environment_url.txt` file, and DAST
automatically parses that file to find its scan target.
You can see an [example](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml)
of this in our Auto DevOps CI YAML.
This is useful for testing in dynamic environments. To run DAST against an application
dynamically created during a GitLab CI/CD pipeline, a job that runs prior to the DAST scan must
persist the application's domain in an `environment_url.txt` file. DAST automatically parses the
`environment_url.txt` file to find its scan target.
For example, in a job that runs prior to DAST, you could include code that looks similar to:
```yaml
script:
- echo http://${CI_PROJECT_ID}-${CI_ENVIRONMENT_SLUG}.domain.com > environment_url.txt
artifacts:
paths: [environment_url.txt]
when: always
```
You can see an example of this in our [Auto DevOps CI YAML](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml) file.
If both values are set, the `DAST_WEBSITE` value takes precedence.

View File

@ -31,8 +31,7 @@ module API
def access_checker_for(actor, protocol)
access_checker_klass.new(actor.key_or_user, container, protocol,
authentication_abilities: ssh_authentication_abilities,
namespace_path: namespace_path,
repository_path: project_path,
repository_path: repository_path,
redirected_path: redirected_path)
end
@ -71,18 +70,22 @@ module API
false
end
def project_path
project&.path || project_path_match[:project_path]
end
def namespace_path
project&.namespace&.full_path || project_path_match[:namespace_path]
end
private
def project_path_match
@project_path_match ||= params[:project].match(Gitlab::PathRegex.full_project_git_path_regex) || {}
def repository_path
if container
"#{container.full_path}.git"
elsif params[:project]
# When the project doesn't exist, we still need to pass on the path
# to support auto-creation in `GitAccessProject`.
#
# For consistency with the Git HTTP controllers, we normalize the path
# to remove a leading slash and ensure a trailing `.git`.
#
# NOTE: For GitLab Shell, `params[:project]` is the full repository path
# from the SSH command, with an optional trailing `.git`.
"#{params[:project].delete_prefix('/').delete_suffix('.git')}.git"
end
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
@ -96,7 +99,7 @@ module API
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
# Project id to pass between components that don't share/don't have
# Repository id to pass between components that don't share/don't have
# access to the same filesystem mounts
def gl_repository
repo_type.identifier_for_container(container)
@ -106,8 +109,9 @@ module API
repository.full_path
end
# Return the repository depending on whether we want the wiki or the
# regular repository
# Return the repository for the detected type and container
#
# @returns [Repository]
def repository
@repository ||= repo_type.repository_for(container)
end

View File

@ -4,7 +4,7 @@ module Constraints
class ProjectUrlConstrainer
def matches?(request, existence_check: true)
namespace_path = request.params[:namespace_id]
project_path = request.params[:project_id] || request.params[:id] || request.params[:repository_id]
project_path = request.params[:project_id] || request.params[:id]
full_path = [namespace_path, project_path].join('/')
return false unless ProjectPathValidator.valid_path?(full_path)

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
module Constraints
class RepositoryRedirectUrlConstrainer
def matches?(request)
path = request.params[:repository_path].delete_suffix('.git')
query = request.query_string
git_request?(query) && container_path?(path)
end
# Allow /info/refs, /info/refs?service=git-upload-pack, and
# /info/refs?service=git-receive-pack, but nothing else.
def git_request?(query)
query.blank? ||
query == 'service=git-upload-pack' ||
query == 'service=git-receive-pack'
end
# Check if the path matches any known repository containers.
# These also cover wikis, since a `.wiki` suffix is valid in project/group paths too.
def container_path?(path)
NamespacePathValidator.valid_path?(path) ||
ProjectPathValidator.valid_path?(path) ||
path =~ Gitlab::PathRegex.full_snippets_repository_path_regex
end
end
end

View File

@ -30,7 +30,7 @@ module Gitlab
Labkit::Context.current.to_h.include?(Labkit::Context.log_key(attribute_name))
end
def initialize(args)
def initialize(**args)
unknown_attributes = args.keys - APPLICATION_ATTRIBUTES.map(&:name)
raise ArgumentError, "#{unknown_attributes} are not known keys" if unknown_attributes.any?

View File

@ -0,0 +1,24 @@
stages:
- build
- test
- deploy
- dast
variables:
DAST_VERSION: 1
# Setting this variable will affect all Security templates
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
dast:
stage: dast
image:
name: "$SECURE_ANALYZERS_PREFIX/dast:$DAST_VERSION"
variables:
GIT_STRATEGY: none
allow_failure: true
script:
- /analyze
artifacts:
reports:
dast: gl-dast-report.json

View File

@ -43,7 +43,7 @@ module Gitlab
ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS
attr_reader :actor, :protocol, :authentication_abilities,
:namespace_path, :redirected_path, :auth_result_type,
:repository_path, :redirected_path, :auth_result_type,
:cmd, :changes
attr_accessor :container
@ -57,21 +57,16 @@ module Gitlab
raise ArgumentError, "No error message defined for #{key}"
end
def initialize(actor, container, protocol, authentication_abilities:, namespace_path: nil, repository_path: nil, redirected_path: nil, auth_result_type: nil)
def initialize(actor, container, protocol, authentication_abilities:, repository_path: nil, redirected_path: nil, auth_result_type: nil)
@actor = actor
@container = container
@protocol = protocol
@authentication_abilities = Array(authentication_abilities)
@namespace_path = namespace_path
@repository_path = repository_path
@redirected_path = redirected_path
@auth_result_type = auth_result_type
end
def repository_path
@repository_path ||= project&.path
end
def check(cmd, changes)
@changes = changes
@cmd = cmd

View File

@ -35,7 +35,19 @@ module Gitlab
end
def namespace
@namespace ||= Namespace.find_by_full_path(namespace_path)
strong_memoize(:namespace) { Namespace.find_by_full_path(namespace_path) }
end
def namespace_path
strong_memoize(:namespace_path) { repository_path_match[:namespace_path] }
end
def project_path
strong_memoize(:project_path) { repository_path_match[:project_path] }
end
def repository_path_match
strong_memoize(:repository_path_match) { repository_path.match(Gitlab::PathRegex.full_project_git_path_regex) || {} }
end
def ensure_project_on_push!
@ -44,7 +56,7 @@ module Gitlab
return unless user&.can?(:create_projects, namespace)
project_params = {
path: repository_path,
path: project_path,
namespace_id: namespace.id,
visibility_level: Gitlab::VisibilityLevel::PRIVATE
}

View File

@ -9,7 +9,7 @@ module Gitlab
field :user_permissions, permission_type,
description: description,
null: false,
resolve: -> (obj, _, _) { obj }
method: :itself
end
end
end

View File

@ -5,7 +5,7 @@ module Gitlab
class ProjectCreator
attr_reader :repo, :name, :namespace, :current_user, :session_data, :type
def initialize(repo, name, namespace, current_user, session_data, type: 'github')
def initialize(repo, name, namespace, current_user, type: 'github', **session_data)
@repo = repo
@name = name
@namespace = namespace

View File

@ -180,12 +180,16 @@ module Gitlab
end
end
def project_git_route_regex
@project_git_route_regex ||= /#{project_route_regex}\.git/.freeze
def repository_route_regex
@repository_route_regex ||= /#{full_namespace_route_regex}|#{personal_snippet_repository_path_regex}/.freeze
end
def project_wiki_git_route_regex
@project_wiki_git_route_regex ||= /#{PATH_REGEX_STR}\.wiki/.freeze
def repository_git_route_regex
@repository_git_route_regex ||= /#{repository_route_regex}\.git/.freeze
end
def repository_wiki_git_route_regex
@repository_wiki_git_route_regex ||= /#{full_namespace_route_regex}\.wiki\.git/.freeze
end
def full_namespace_path_regex
@ -250,10 +254,6 @@ module Gitlab
%r{\A(#{personal_snippet_repository_path_regex}|#{project_snippet_repository_path_regex})\z}
end
def personal_and_project_snippets_path_regex
%r{#{personal_snippet_path_regex}|#{project_snippet_path_regex}}
end
def container_image_regex
@container_image_regex ||= %r{([\w\.-]+\/){0,1}[\w\.-]+}.freeze
end

View File

@ -5,7 +5,7 @@ module Gitlab
NotFoundError = Class.new(StandardError)
def self.parse(path)
repo_path = path.sub(/\.git\z/, '').sub(%r{\A/}, '')
repo_path = path.delete_prefix('/').delete_suffix('.git')
redirected_path = nil
# Detect the repo type based on the path, the first one tried is the project

View File

@ -81,7 +81,10 @@ namespace :gitlab do
if head_assets_md5 != master_assets_md5 || !public_assets_webpack_dir_exists
FileUtils.rm_r(Tasks::Gitlab::Assets::PUBLIC_ASSETS_WEBPACK_DIR) if public_assets_webpack_dir_exists
system('yarn webpack')
unless system('yarn webpack')
abort 'Error: Unable to compile webpack production bundle.'.color(:red)
end
end
end

View File

@ -9525,6 +9525,9 @@ msgstr ""
msgid "DevopsAdoptionSegment|The maximum number of segments has been reached"
msgstr ""
msgid "DevopsAdoptionSegment|The maximum number of selections has been reached"
msgstr ""
msgid "DevopsAdoption|%{selectedCount} group selected (20 max)"
msgstr ""
@ -16491,6 +16494,9 @@ msgstr ""
msgid "ManualOrdering|Couldn't save the order of the issues"
msgstr ""
msgid "Manually link this issue by adding it to the linked issue section of the %{originating_vulnerability}."
msgstr ""
msgid "Map a FogBugz account ID to a GitLab user"
msgstr ""
@ -27815,9 +27821,6 @@ msgstr ""
msgid "This issue is currently blocked by the following issues:"
msgstr ""
msgid "This issue is currently blocked by the following issues: %{issues}."
msgstr ""
msgid "This issue is in a child epic of the filtered epic"
msgstr ""
@ -28977,6 +28980,9 @@ msgstr ""
msgid "Unable to convert Kubernetes logs encoding to UTF-8"
msgstr ""
msgid "Unable to create link to vulnerability"
msgstr ""
msgid "Unable to fetch unscanned projects"
msgstr ""
@ -32815,6 +32821,9 @@ msgstr ""
msgid "or"
msgstr ""
msgid "originating vulnerability"
msgstr ""
msgid "out of %d total test"
msgid_plural "out of %d total tests"
msgstr[0] ""

View File

@ -367,8 +367,8 @@ RSpec.describe OmniauthCallbacksController, type: :controller do
before do
stub_last_request_id(last_request_id)
stub_omniauth_saml_config({ enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'],
providers: [saml_config] })
stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'],
providers: [saml_config])
mock_auth_hash_with_saml_xml('saml', +'my-uid', user.email, mock_saml_response)
request.env['devise.mapping'] = Devise.mappings[:user]
request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth']

View File

@ -1128,12 +1128,12 @@ RSpec.describe Projects::IssuesController do
{ merge_request_to_resolve_discussions_of: merge_request.iid }
end
def post_issue(issue_params, other_params: {})
def post_issue(other_params: {}, **issue_params)
post :create, params: { namespace_id: project.namespace.to_param, project_id: project, issue: issue_params, merge_request_to_resolve_discussions_of: merge_request.iid }.merge(other_params)
end
it 'creates an issue for the project' do
expect { post_issue({ title: 'Hello' }) }.to change { project.issues.reload.size }.by(1)
expect { post_issue(title: 'Hello') }.to change { project.issues.reload.size }.by(1)
end
it "doesn't overwrite given params" do
@ -1157,7 +1157,7 @@ RSpec.describe Projects::IssuesController do
describe "resolving a single discussion" do
before do
post_issue({ title: 'Hello' }, other_params: { discussion_to_resolve: discussion.id })
post_issue(title: 'Hello', other_params: { discussion_to_resolve: discussion.id })
end
it 'resolves a single discussion' do
discussion.first_note.reload

View File

@ -9,28 +9,20 @@ RSpec.describe Repositories::GitHttpController do
let_it_be(:personal_snippet) { create(:personal_snippet, :public, :repository) }
let_it_be(:project_snippet) { create(:project_snippet, :public, :repository, project: project) }
let(:namespace_id) { project.namespace.to_param }
let(:repository_id) { project.path + '.git' }
let(:container_params) do
{
namespace_id: namespace_id,
repository_id: repository_id
}
end
shared_examples Repositories::GitHttpController do
let(:repository_path) { "#{container.full_path}.git" }
let(:params) { { repository_path: repository_path } }
let(:params) { container_params }
describe 'HEAD #info_refs' do
it 'returns 403' do
head :info_refs, params: params
describe 'HEAD #info_refs' do
it 'returns 403' do
head :info_refs, params: params
expect(response).to have_gitlab_http_status(:forbidden)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
shared_examples 'info_refs behavior' do
describe 'GET #info_refs' do
let(:params) { container_params.merge(service: 'git-upload-pack') }
let(:params) { super().merge(service: 'git-upload-pack') }
it 'returns 401 for unauthenticated requests to public repositories when http protocol is disabled' do
stub_application_setting(enabled_git_access_protocol: 'ssh')
@ -43,6 +35,26 @@ RSpec.describe Repositories::GitHttpController do
expect(response).to have_gitlab_http_status(:unauthorized)
end
it 'calls the right access checker class with the right object' do
allow(controller).to receive(:verify_workhorse_api!).and_return(true)
access_double = double
options = {
authentication_abilities: [:download_code],
repository_path: repository_path,
redirected_path: nil,
auth_result_type: :none
}
expect(access_checker_class).to receive(:new)
.with(nil, container, 'http', hash_including(options))
.and_return(access_double)
allow(access_double).to receive(:check).and_return(false)
get :info_refs, params: params
end
context 'with authorized user' do
before do
request.headers.merge! auth_env(user.username, user.password, nil)
@ -97,54 +109,38 @@ RSpec.describe Repositories::GitHttpController do
end
end
end
end
shared_examples 'git_upload_pack behavior' do |expected|
describe 'POST #git_upload_pack' do
before do
allow(controller).to receive(:authenticate_user).and_return(true)
allow(controller).to receive(:verify_workhorse_api!).and_return(true)
allow(controller).to receive(:access_check).and_return(nil)
end
def send_request
it 'returns 200' do
post :git_upload_pack, params: params
end
context 'on a read-only instance' do
expect(response).to have_gitlab_http_status(:ok)
end
end
end
context 'when repository container is a project' do
it_behaves_like Repositories::GitHttpController do
let(:container) { project }
let(:user) { project.owner }
let(:access_checker_class) { Gitlab::GitAccess }
describe 'POST #git_upload_pack' do
before do
allow(Gitlab::Database).to receive(:read_only?).and_return(true)
allow(controller).to receive(:verify_workhorse_api!).and_return(true)
end
it 'does not update project statistics' do
expect(ProjectDailyStatisticsWorker).not_to receive(:perform_async)
send_request
def send_request
post :git_upload_pack, params: params
end
end
if expected
context 'when project_statistics_sync feature flag is disabled' do
context 'on a read-only instance' do
before do
stub_feature_flags(project_statistics_sync: false)
end
it 'updates project statistics async' do
expect(ProjectDailyStatisticsWorker).to receive(:perform_async)
send_request
end
end
it 'updates project statistics sync' do
expect { send_request }.to change {
Projects::DailyStatisticsFinder.new(project).total_fetch_count
}.from(0).to(1)
end
else
context 'when project_statistics_sync feature flag is disabled' do
before do
stub_feature_flags(project_statistics_sync: false)
allow(Gitlab::Database).to receive(:read_only?).and_return(true)
end
it 'does not update project statistics' do
@ -154,68 +150,48 @@ RSpec.describe Repositories::GitHttpController do
end
end
it 'does not update project statistics' do
expect { send_request }.not_to change {
Projects::DailyStatisticsFinder.new(project).total_fetch_count
}.from(0)
context 'when project_statistics_sync feature flag is disabled' do
before do
stub_feature_flags(project_statistics_sync: false)
end
it 'updates project statistics async for projects' do
expect(ProjectDailyStatisticsWorker).to receive(:perform_async)
send_request
end
end
it 'updates project statistics sync for projects' do
expect { send_request }.to change {
Projects::DailyStatisticsFinder.new(container).total_fetch_count
}.from(0).to(1)
end
end
end
end
shared_examples 'access checker class' do
let(:params) { container_params.merge(service: 'git-upload-pack') }
it 'calls the right access class checker with the right object' do
allow(controller).to receive(:verify_workhorse_api!).and_return(true)
access_double = double
expect(expected_class).to receive(:new).with(anything, expected_object, 'http', anything).and_return(access_double)
allow(access_double).to receive(:check).and_return(false)
get :info_refs, params: params
end
end
context 'when repository container is a project' do
it_behaves_like 'info_refs behavior' do
context 'when repository container is a project wiki' do
it_behaves_like Repositories::GitHttpController do
let(:container) { create(:project_wiki, :empty_repo, project: project) }
let(:user) { project.owner }
end
it_behaves_like 'git_upload_pack behavior', true
it_behaves_like 'access checker class' do
let(:expected_class) { Gitlab::GitAccess }
let(:expected_object) { project }
let(:access_checker_class) { Gitlab::GitAccessWiki }
end
end
context 'when repository container is a personal snippet' do
let(:namespace_id) { 'snippets' }
let(:repository_id) { personal_snippet.to_param + '.git' }
it_behaves_like 'info_refs behavior' do
it_behaves_like Repositories::GitHttpController do
let(:container) { personal_snippet }
let(:user) { personal_snippet.author }
end
it_behaves_like 'git_upload_pack behavior', false
it_behaves_like 'access checker class' do
let(:expected_class) { Gitlab::GitAccessSnippet }
let(:expected_object) { personal_snippet }
let(:access_checker_class) { Gitlab::GitAccessSnippet }
end
end
context 'when repository container is a project snippet' do
let(:namespace_id) { project.full_path + '/snippets' }
let(:repository_id) { project_snippet.to_param + '.git' }
it_behaves_like 'info_refs behavior' do
it_behaves_like Repositories::GitHttpController do
let(:container) { project_snippet }
let(:user) { project_snippet.author }
end
it_behaves_like 'git_upload_pack behavior', false
it_behaves_like 'access checker class' do
let(:expected_class) { Gitlab::GitAccessSnippet }
let(:expected_object) { project_snippet }
let(:access_checker_class) { Gitlab::GitAccessSnippet }
end
end
end

View File

@ -23,8 +23,7 @@ RSpec.describe Repositories::LfsStorageController do
let(:params) do
{
namespace_id: project.namespace.path,
repository_id: "#{project.path}.git",
repository_path: "#{project.full_path}.git",
oid: '6b9765d3888aaec789e8c309eb05b05c3a87895d6ad70d2264bd7270fff665ac',
size: '6725030'
}

View File

@ -114,6 +114,113 @@ RSpec.describe UsersController do
end
end
describe 'GET #activity' do
context 'with rendered views' do
render_views
describe 'when logged in' do
before do
sign_in(user)
end
it 'renders the show template' do
get :show, params: { username: user.username }
expect(response).to be_successful
expect(response).to render_template('show')
end
end
describe 'when logged out' do
it 'renders the show template' do
get :activity, params: { username: user.username }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('show')
end
end
end
context 'when public visibility level is restricted' do
before do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end
context 'when logged out' do
it 'redirects to login page' do
get :activity, params: { username: user.username }
expect(response).to redirect_to new_user_session_path
end
end
context 'when logged in' do
before do
sign_in(user)
end
it 'renders show' do
get :activity, params: { username: user.username }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('show')
end
end
end
context 'when a user by that username does not exist' do
context 'when logged out' do
it 'redirects to login page' do
get :activity, params: { username: 'nonexistent' }
expect(response).to redirect_to new_user_session_path
end
end
context 'when logged in' do
before do
sign_in(user)
end
it 'renders 404' do
get :activity, params: { username: 'nonexistent' }
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'json with events' do
let(:project) { create(:project) }
before do
project.add_developer(user)
Gitlab::DataBuilder::Push.build_sample(project, user)
sign_in(user)
end
it 'loads events' do
get :activity, params: { username: user }, format: :json
expect(assigns(:events)).not_to be_empty
end
it 'hides events if the user cannot read cross project' do
allow(Ability).to receive(:allowed?).and_call_original
expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
get :activity, params: { username: user }, format: :json
expect(assigns(:events)).to be_empty
end
it 'hides events if the user has a private profile' do
Gitlab::DataBuilder::Push.build_sample(project, private_user)
get :activity, params: { username: private_user.username }, format: :json
expect(assigns(:events)).to be_empty
end
end
end
describe 'GET #calendar' do
context 'for user' do
let(:project) { create(:project) }

View File

@ -1,18 +0,0 @@
# frozen_string_literal: true
FactoryBot.define do
factory :devops_adoption_segment_selection, class: 'Analytics::DevopsAdoption::SegmentSelection' do
association :segment, factory: :devops_adoption_segment
project
trait :project do
group { nil }
project
end
trait :group do
project { nil }
group
end
end
end

View File

@ -1,7 +0,0 @@
# frozen_string_literal: true
FactoryBot.define do
factory :devops_adoption_segment, class: 'Analytics::DevopsAdoption::Segment' do
sequence(:name) { |n| "Segment #{n}" }
end
end

View File

@ -13,5 +13,6 @@ FactoryBot.define do
sequence(:past_time) { |n| 4.hours.ago + (2 * n).seconds }
sequence(:iid)
sequence(:sha) { |n| Digest::SHA1.hexdigest("commit-like-#{n}") }
sequence(:oid) { |n| Digest::SHA2.hexdigest("oid-like-#{n}") }
sequence(:variable) { |n| "var#{n}" }
end

View File

@ -129,4 +129,48 @@ RSpec.describe GroupMembersFinder, '#execute' do
expect(result.to_a).not_to include(member_with_2fa)
expect(result.to_a).to match_array([member1, member2])
end
it 'returns direct members with two-factor auth if requested by owner' do
group.add_owner(user1)
group.add_maintainer(user2)
nested_group.add_maintainer(user3)
member_with_2fa = nested_group.add_maintainer(user5)
result = described_class.new(nested_group, user1, params: { two_factor: 'enabled' }).execute(include_relations: [:direct])
expect(result.to_a).to match_array([member_with_2fa])
end
it 'returns inherited members with two-factor auth if requested by owner' do
group.add_owner(user1)
member_with_2fa = group.add_maintainer(user5)
nested_group.add_maintainer(user2)
nested_group.add_maintainer(user3)
result = described_class.new(nested_group, user1, params: { two_factor: 'enabled' }).execute(include_relations: [:inherited])
expect(result.to_a).to match_array([member_with_2fa])
end
it 'returns direct members without two-factor auth if requested by owner' do
group.add_owner(user1)
group.add_maintainer(user2)
member3 = nested_group.add_maintainer(user3)
nested_group.add_maintainer(user5)
result = described_class.new(nested_group, user1, params: { two_factor: 'disabled' }).execute(include_relations: [:direct])
expect(result.to_a).to match_array([member3])
end
it 'returns inherited members without two-factor auth if requested by owner' do
member1 = group.add_owner(user1)
group.add_maintainer(user5)
nested_group.add_maintainer(user2)
nested_group.add_maintainer(user3)
result = described_class.new(nested_group, user1, params: { two_factor: 'disabled' }).execute(include_relations: [:inherited])
expect(result.to_a).to match_array([member1])
end
end

View File

@ -37,7 +37,7 @@ describe('BoardCardAssigneeDropdown', () => {
data() {
return {
search,
selected: store.getters.activeIssue.assignees,
selected: [],
participants,
};
},
@ -63,14 +63,13 @@ describe('BoardCardAssigneeDropdown', () => {
[getIssueParticipants, getIssueParticipantsSpy],
[searchUsers, getSearchUsersSpy],
]);
wrapper = mount(BoardAssigneeDropdown, {
localVue,
apolloProvider: fakeApollo,
data() {
return {
search,
selected: store.getters.activeIssue.assignees,
selected: [],
participants,
};
},
@ -369,4 +368,18 @@ describe('BoardCardAssigneeDropdown', () => {
expect(findByText(currentUser.username).exists()).toBe(true);
});
});
describe('when setting an assignee', () => {
beforeEach(() => {
createComponent();
});
it('passes loading state from Vuex to BoardEditableItem', async () => {
store.state.isSettingAssignees = true;
await wrapper.vm.$nextTick();
expect(wrapper.find(BoardEditableItem).props('loading')).toBe(true);
});
});
});

View File

@ -690,10 +690,18 @@ describe('setAssignees', () => {
{},
{ activeIssue: { iid, referencePath: refPath }, commit: () => {} },
[
{
type: 'SET_ASSIGNEE_LOADING',
payload: true,
},
{
type: 'UPDATE_ISSUE_BY_ID',
payload: { prop: 'assignees', issueId: undefined, value: [node] },
},
{
type: 'SET_ASSIGNEE_LOADING',
payload: false,
},
],
[],
done,

Some files were not shown because too many files have changed in this diff Show More