Commit 10b7a9d1 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 09a529ad 24e815ab
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class MergeRequestConnectionType < Types::CountableConnectionType
field :total_time_to_merge, GraphQL::FLOAT_TYPE, null: true,
description: 'Total sum of time to merge, in seconds, for the collection of merge requests'
# rubocop: disable CodeReuse/ActiveRecord
def total_time_to_merge
object.items.reorder(nil).total_time_to_merge
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
......@@ -4,7 +4,7 @@ module Types
class MergeRequestType < BaseObject
graphql_name 'MergeRequest'
connection_type_class(Types::CountableConnectionType)
connection_type_class(Types::MergeRequestConnectionType)
implements(Types::Notes::NoteableType)
implements(Types::CurrentUserTodos)
......
......@@ -339,6 +339,13 @@ class MergeRequest < ApplicationRecord
)
end
def self.total_time_to_merge
join_metrics
.merge(MergeRequest::Metrics.with_valid_time_to_merge)
.pluck(MergeRequest::Metrics.time_to_merge_expression)
.first
end
after_save :keep_around_commit, unless: :importing?
alias_attribute :project, :target_project
......
......@@ -10,6 +10,11 @@ class MergeRequest::Metrics < ApplicationRecord
scope :merged_after, ->(date) { where(arel_table[:merged_at].gteq(date)) }
scope :merged_before, ->(date) { where(arel_table[:merged_at].lteq(date)) }
scope :with_valid_time_to_merge, -> { where(arel_table[:merged_at].gt(arel_table[:created_at])) }
def self.time_to_merge_expression
Arel.sql('EXTRACT(epoch FROM SUM(AGE(merge_request_metrics.merged_at, merge_request_metrics.created_at)))')
end
private
......
---
title: Add merge requests total time to merge field to the GraphQL API
merge_request: 46040
author:
type: added
# frozen_string_literal: true
class AddIndexToMergeRequestMetricsTargetProjectId < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_mr_metrics_on_target_project_id_merged_at_time_to_merge'
disable_ddl_transaction!
def up
add_concurrent_index :merge_request_metrics, [:target_project_id, :merged_at, :created_at], where: 'merged_at > created_at', name: INDEX_NAME
end
def down
remove_concurrent_index_by_name(:merge_request_metrics, INDEX_NAME)
end
end
bde71afbe34006eedbd97ac457df31b247fc89a572ca8900c60b16c4d6a8ef93
\ No newline at end of file
......@@ -21631,6 +21631,8 @@ CREATE UNIQUE INDEX index_mr_context_commits_on_merge_request_id_and_sha ON merg
CREATE INDEX index_mr_metrics_on_target_project_id_merged_at_nulls_last ON merge_request_metrics USING btree (target_project_id, merged_at DESC NULLS LAST, id DESC);
CREATE INDEX index_mr_metrics_on_target_project_id_merged_at_time_to_merge ON merge_request_metrics USING btree (target_project_id, merged_at, created_at) WHERE (merged_at > created_at);
CREATE UNIQUE INDEX index_namespace_aggregation_schedules_on_namespace_id ON namespace_aggregation_schedules USING btree (namespace_id);
CREATE INDEX index_namespace_onboarding_actions_on_namespace_id ON namespace_onboarding_actions USING btree (namespace_id);
......
......@@ -13050,6 +13050,11 @@ type MergeRequestConnection {
Information to aid in pagination.
"""
pageInfo: PageInfo!
"""
Total sum of time to merge, in seconds, for the collection of merge requests
"""
totalTimeToMerge: Float
}
"""
......
......@@ -36011,6 +36011,20 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "totalTimeToMerge",
"description": "Total sum of time to merge, in seconds, for the collection of merge requests",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Float",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
......@@ -434,7 +434,7 @@ GitLab CI/CD is the open-source continuous integration service included with Git
#### GitLab Shell
- [Project page](https://gitlab.com/gitlab-org/gitlab-shell/blob/master/README.md)
- [Project page](https://gitlab.com/gitlab-org/gitlab-shell/-/blob/main/README.md)
- Configuration:
- [Omnibus](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template)
- [Charts](https://docs.gitlab.com/charts/charts/gitlab/gitlab-shell/)
......
......@@ -503,10 +503,6 @@ is persisted.
Make sure behavior under feature flag doesn't go untested in some non-specific contexts.
See the
[testing guide](../testing_guide/best_practices.md#feature-flags-in-tests)
for information and examples on how to stub feature flags in tests.
### `stub_feature_flags: false`
This disables a memory-stubbed flipper, and uses `Flipper::Adapters::ActiveRecord`
......
......@@ -349,3 +349,9 @@ Where `<test_file>` is:
- `qa/specs/features/browser_ui/1_manage/login/login_spec.rb` when running the Login example.
- `qa/specs/features/browser_ui/2_plan/issues/issue_spec.rb` when running the Issue example.
## End-to-end test merge request template
When submitting a new end-to-end test, use the ["New End to End Test"](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/merge_request_templates/New%20End%20To%20End%20Test.md)
merge request description template for additional
steps that are required prior a successful merge.
......@@ -47,9 +47,9 @@ and allows you to comment on a change.
You can create an issue for a vulnerability by selecting the **Create issue** button.
This creates a [confidential issue](../../project/issues/confidential_issues.md) in the
project the vulnerability came from and pre-populates it with useful information from
the vulnerability report. After the issue is created, GitLab redirects you to the
This allows the user to create a [confidential issue](../../project/issues/confidential_issues.md)
in the project the vulnerability came from. Fields are pre-populated with pertinent information
from the vulnerability report. After the issue is created, GitLab redirects you to the
issue page so you can edit, assign, or comment on the issue.
## Link issues to the vulnerability
......
......@@ -240,6 +240,10 @@ Users can unlink SAML for a group from their profile page. This can be helpful i
- You no longer want a group to be able to sign you in to GitLab.com.
- Your SAML NameID has changed and so GitLab can no longer find your user.
CAUTION: **Warning:**
Unlinking an account removes all roles assigned to that user within the group.
If a user relinks their account, roles need to be reassigned.
For example, to unlink the `MyOrg` account, the following **Disconnect** button is available under **Profile > Accounts**:
![Unlink Group SAML](img/unlink_group_saml.png)
......@@ -280,10 +284,6 @@ the user gets the highest access level from the groups. For example, if one grou
is linked as `Guest` and another `Maintainer`, a user in both groups gets `Maintainer`
access.
CAUTION: **Warning:**
Unlinking an account removes all roles assigned to that user within the group.
If a user relinks their account, roles need to be reassigned.
## Glossary
| Term | Description |
......
......@@ -159,6 +159,7 @@ The following table depicts the various user permission levels in a project.
| Manage Terraform state | | | | ✓ | ✓ |
| Manage license policy **(ULTIMATE)** | | | | ✓ | ✓ |
| Edit comments (posted by any user) | | | | ✓ | ✓ |
| Reposition comments on images (posted by any user)|✓ (*11*) | ✓ (*11*) | ✓ (*11*) | ✓ | ✓ |
| Manage Error Tracking | | | | ✓ | ✓ |
| Delete wiki pages | | | | ✓ | ✓ |
| View project Audit Events | | | | ✓ | ✓ |
......@@ -188,6 +189,7 @@ The following table depicts the various user permission levels in a project.
1. For information on eligible approvers for merge requests, see
[Eligible approvers](project/merge_requests/merge_request_approvals.md#eligible-approvers).
1. Owner permission is only available at the group or personal namespace level (and for instance admins) and is inherited by its projects.
1. Applies only to comments on [Design Management](project/issues/design_management.md) designs.
## Project features permissions
......@@ -405,6 +407,11 @@ automatically have access to projects and subgroups underneath. To support such
Users with minimal access can list the group in the UI and through the API. However, they cannot see
details such as projects or subgroups. They do not have access to the group's page or list any of its subgroups or projects.
### Minimal access users take license seats
Users with even a "minimal access" role are counted against your number of license seats. This
requirement does not apply for [GitLab Gold/Ultimate](https://about.gitlab.com/pricing/) subscriptions.
## Project features
Project features like wiki and issues can be hidden from users depending on
......
......@@ -149,6 +149,9 @@ belong either to the specific group or to one of its subgroups.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an overview, see [Group Deploy Tokens](https://youtu.be/8kxTJvaD9ks).
The Group Deploy Tokens UI is now accessible under **Settings > Repository**,
not **Settings > CI/CD** as indicated in the video.
To use a group deploy token:
1. [Create](#creating-a-deploy-token) a deploy token for a group.
......
......@@ -198,8 +198,8 @@ To rename a repository:
1. Navigate to your project's **Settings > General**.
1. Under **Advanced**, click **Expand**.
1. Under "Rename repository", change the "Path" to your liking.
1. Hit **Rename project**.
1. Under **Change path**, update the repository's path.
1. Click **Change path**.
Remember that this can have unintended side effects since everyone with the
old URL won't be able to push or pull. Read more about what happens with the
......
<script>
import axios from 'axios';
import { GlButton, GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { GlButton } from '@gitlab/ui';
import RelatedIssuesStore from '~/related_issues/stores/related_issues_store';
import RelatedIssuesBlock from '~/related_issues/components/related_issues_block.vue';
import { issuableTypesMap, PathIdSeparator } from '~/related_issues/constants';
......@@ -15,9 +15,6 @@ export default {
components: {
RelatedIssuesBlock,
GlButton,
GlAlert,
GlSprintf,
GlLink,
},
props: {
endpoint: {
......@@ -60,7 +57,7 @@ export default {
return Boolean(this.state.relatedIssues.find(i => i.lockIssueRemoval));
},
canCreateIssue() {
return !this.isIssueAlreadyCreated && !this.isFetching && Boolean(this.createIssueUrl);
return !this.isIssueAlreadyCreated && !this.isFetching && Boolean(this.newIssueUrl);
},
},
inject: {
......@@ -70,7 +67,7 @@ export default {
projectFingerprint: {
default: '',
},
createIssueUrl: {
newIssueUrl: {
default: '',
},
reportType: {
......@@ -89,17 +86,7 @@ export default {
methods: {
createIssue() {
this.isProcessingAction = true;
this.errorCreatingIssue = false;
return axios
.post(this.createIssueUrl)
.then(({ data: { web_url } }) => {
redirectTo(web_url);
})
.catch(() => {
this.isProcessingAction = false;
this.errorCreatingIssue = true;
});
redirectTo(this.newIssueUrl, { params: { vulnerability_id: this.vulnerabilityId } });
},
toggleFormVisibility() {
this.isFormVisible = !this.isFormVisible;
......@@ -218,28 +205,6 @@ export default {
<template>
<div>
<gl-alert
v-if="errorCreatingIssue"
variant="danger"
class="gl-mt-5"
@dismiss="errorCreatingIssue = false"
>
<p class="gl-font-weight-bold gl-mb-2">{{ $options.i18n.createIssueErrorTitle }}</p>
<p class="gl-mb-0">
<gl-sprintf :message="$options.i18n.createIssueErrorBody">
<template #tracking="{ content }">
<gl-link class="gl-display-inline-block" :href="issueTrackingHelpPath" target="_blank">
{{ content }}
</gl-link>
</template>
<template #permissions="{ content }">
<gl-link class="gl-display-inline-block" :href="permissionsHelpPath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</p>
</gl-alert>
<related-issues-block
:help-path="helpPath"
:is-fetching="isFetching"
......
......@@ -15,7 +15,7 @@ export default el => {
provide: {
reportType: vulnerability.reportType,
createIssueUrl: vulnerability.createIssueUrl,
newIssueUrl: vulnerability.newIssueUrl,
projectFingerprint: vulnerability.projectFingerprint,
vulnerabilityId: vulnerability.id,
issueTrackingHelpPath: vulnerability.issueTrackingHelpPath,
......
......@@ -10,7 +10,7 @@ module VulnerabilitiesHelper
result = {
timestamp: Time.now.to_i,
create_issue_url: create_issue_url_for(vulnerability),
new_issue_url: new_issue_url_for(vulnerability),
create_jira_issue_url: create_jira_issue_url_for(vulnerability),
related_jira_issues_path: project_integrations_jira_issues_path(vulnerability.project, vulnerability_ids: [vulnerability.id]),
has_mr: !!vulnerability.finding.merge_request_feedback.try(:merge_request_iid),
......@@ -27,10 +27,10 @@ module VulnerabilitiesHelper
result.merge(vulnerability_data(vulnerability), vulnerability_finding_data(vulnerability))
end
def create_issue_url_for(vulnerability)
def new_issue_url_for(vulnerability)
return unless vulnerability.project.issues_enabled?
create_issue_project_security_vulnerability_path(vulnerability.project, vulnerability)
new_project_issue_path(vulnerability.project, { vulnerability_id: vulnerability.id })
end
def create_jira_issue_url_for(vulnerability)
......
---
title: Creating an issue from a vulnerability takes user to the new issue page
merge_request: 48926
author:
type: changed
......@@ -30,7 +30,7 @@ describe('Vulnerability Header', () => {
reportType: 'sast',
state: 'detected',
createMrUrl: '/create_mr_url',
createIssueUrl: '/create_issue_url',
newIssueUrl: '/new_issue_url',
projectFingerprint: 'abc123',
pipeline: {
id: 2,
......
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import RelatedIssues from 'ee/vulnerabilities/components/related_issues.vue';
import waitForPromises from 'helpers/wait_for_promises';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
......@@ -25,7 +23,7 @@ describe('Vulnerability related issues component', () => {
};
const vulnerabilityId = 5131;
const createIssueUrl = '/create/issue';
const newIssueUrl = '/new/issue';
const projectFingerprint = 'project-fingerprint';
const issueTrackingHelpPath = '/help/issue/tracking';
const permissionsHelpPath = '/help/permissions';
......@@ -40,7 +38,7 @@ describe('Vulnerability related issues component', () => {
provide: {
vulnerabilityId,
projectFingerprint,
createIssueUrl,
newIssueUrl,
reportType,
issueTrackingHelpPath,
permissionsHelpPath,
......@@ -59,7 +57,6 @@ describe('Vulnerability related issues component', () => {
const blockProp = prop => relatedIssuesBlock().props(prop);
const blockEmit = (eventName, data) => relatedIssuesBlock().vm.$emit(eventName, data);
const findCreateIssueButton = () => wrapper.find({ ref: 'createIssue' });
const findAlert = () => wrapper.find(GlAlert);
afterEach(() => {
wrapper.destroy();
......@@ -283,14 +280,10 @@ describe('Vulnerability related issues component', () => {
});
describe('when linked issue is not yet created', () => {
const failCreateIssueAction = async () => {
mockAxios.onPost(createIssueUrl).reply(500);
expect(findAlert().exists()).toBe(false);
findCreateIssueButton().vm.$emit('click');
await waitForPromises();
};
let redirectToSpy;
beforeEach(async () => {
redirectToSpy = jest.spyOn(urlUtility, 'redirectTo').mockImplementation(() => {});
mockAxios.onGet(propsData.endpoint).replyOnce(httpStatusCodes.OK, [issue1, issue2]);
createWrapper({ stubs: { RelatedIssuesBlock } });
await axios.waitForAll();
......@@ -300,34 +293,11 @@ describe('Vulnerability related issues component', () => {
expect(findCreateIssueButton().exists()).toBe(true);
});
it('calls create issue endpoint on click and redirects to new issue', async () => {
const issueUrl = `/group/project/-/security/vulnerabilities/${vulnerabilityId}/create_issue`;
const spy = jest.spyOn(urlUtility, 'redirectTo');
mockAxios.onPost(propsData.createIssueUrl).reply(200, {
web_url: issueUrl,
});
it('calls new issue endpoint on click', () => {
findCreateIssueButton().vm.$emit('click');
await waitForPromises();
const [postRequest] = mockAxios.history.post;
expect(mockAxios.history.post).toHaveLength(1);
expect(postRequest.url).toBe(createIssueUrl);
expect(spy).toHaveBeenCalledWith(issueUrl);
});
it('shows an error message when issue creation fails', async () => {
await failCreateIssueAction();
expect(mockAxios.history.post).toHaveLength(1);
expect(findAlert().exists()).toBe(true);
});
it('dismisses the error message', async () => {
await failCreateIssueAction();
findAlert().vm.$emit('dismiss');
await wrapper.vm.$nextTick();
expect(findAlert().exists()).toBe(false);
expect(redirectToSpy).toHaveBeenCalledWith(newIssueUrl, {
params: { vulnerability_id: vulnerabilityId },
});
});
});
......@@ -335,7 +305,7 @@ describe('Vulnerability related issues component', () => {
it('hides the "Create Issue" button', () => {
createWrapper({
provide: {
createIssueUrl: undefined,
newIssueUrl: undefined,
},
});
......
......@@ -16,7 +16,7 @@ describe('Vulnerability', () => {
report_type: 'sast',
state: 'detected',
create_mr_url: '/create_mr_url',
create_issue_url: '/create_issue_url',
new_issue_url: '/new_issue_url',
project_fingerprint: 'abc123',
pipeline: {
id: 2,
......
......@@ -58,7 +58,7 @@ RSpec.describe VulnerabilitiesHelper do
it 'has expected vulnerability properties' do
expect(subject).to include(
timestamp: Time.now.to_i,
create_issue_url: "/#{project.full_path}/-/security/vulnerabilities/#{vulnerability.id}/create_issue",
new_issue_url: "/#{project.full_path}/-/issues/new?vulnerability_id=#{vulnerability.id}",
create_jira_issue_url: nil,
related_jira_issues_path: "/#{project.full_path}/-/integrations/jira/issues?vulnerability_ids%5B%5D=#{vulnerability.id}",
has_mr: anything,
......@@ -76,8 +76,8 @@ RSpec.describe VulnerabilitiesHelper do
allow(project).to receive(:issues_enabled?).and_return(false)
end
it 'has `create_issue_url` set as nil' do
expect(subject).to include(create_issue_url: nil)
it 'has `new_issue_url` set as nil' do
expect(subject).to include(new_issue_url: nil)
end
end
end
......
......@@ -52,7 +52,7 @@ FactoryBot.define do
after(:build) do |merge_request, evaluator|
metrics = merge_request.build_metrics
metrics.merged_at = 1.week.ago
metrics.merged_at = 1.week.from_now
metrics.merged_by = evaluator.merged_by
metrics.pipeline = create(:ci_empty_pipeline)
end
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['MergeRequestConnection'] do
RSpec.describe GitlabSchema.types['PipelineConnection'] do
it 'has the expected fields' do
expected_fields = %i[count page_info edges nodes]
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['MergeRequestConnection'] do
it 'has the expected fields' do
expected_fields = %i[count totalTimeToMerge page_info edges nodes]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end
......@@ -500,6 +500,77 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
end
describe 'time to merge calculations' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let!(:mr1) do
create(
:merge_request,
:with_merged_metrics,
source_project: project,
target_project: project
)
end
let!(:mr2) do
create(
:merge_request,
:with_merged_metrics,
source_project: project,
target_project: project
)
end
let!(:mr3) do
create(
:merge_request,
:with_merged_metrics,
source_project: project,
target_project: project
)
end
let!(:unmerged_mr) do
create(
:merge_request,
source_project: project,
target_project: project
)
end
before do
project.add_user(user, :developer)
end
describe '.total_time_to_merge' do
it 'returns the sum of the time to merge for all merged MRs' do
mrs = project.merge_requests
expect(mrs.total_time_to_merge).to be_within(1).of(expected_total_time(mrs))
end
context 'when merged_at is earlier than created_at' do
before do
mr1.metrics.update!(merged_at: mr1.metrics.created_at - 1.week)
end
it 'returns nil' do
mrs = project.merge_requests.where(id: mr1.id)
expect(mrs.total_time_to_merge).to be_nil
end
end
def expected_total_time(mrs)
mrs = mrs.reject { |mr| mr.merged_at.nil? }
mrs.reduce(0.0) do |sum, mr|
(mr.merged_at - mr.created_at) + sum
end
end
end
end
describe '#target_branch_sha' do
let(:project) { create(:project, :repository) }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment