Commit 996c6bf0 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 0a0e82d1
...@@ -142,11 +142,25 @@ export const stripHtml = (string, replace = '') => { ...@@ -142,11 +142,25 @@ export const stripHtml = (string, replace = '') => {
}; };
/** /**
* Converts snake_case string to camelCase * Converts a snake_cased string to camelCase.
* Leading and trailing underscores are ignored.
* *
* @param {*} string * @param {String} string The snake_cased string to convert
* @returns {String} A camelCased version of the string
*
* @example
*
* // returns "aSnakeCasedString"
* convertToCamelCase('a_snake_cased_string')
*
* // returns "_leadingUnderscore"
* convertToCamelCase('_leading_underscore')
*
* // returns "trailingUnderscore_"
* convertToCamelCase('trailing_underscore_')
*/ */
export const convertToCamelCase = string => string.replace(/(_\w)/g, s => s[1].toUpperCase()); export const convertToCamelCase = string =>
string.replace(/([a-z0-9])_([a-z0-9])/gi, (match, p1, p2) => `${p1}${p2.toUpperCase()}`);
/** /**
* Converts camelCase string to snake_case * Converts camelCase string to snake_case
......
...@@ -20,10 +20,10 @@ export default { ...@@ -20,10 +20,10 @@ export default {
}, },
computed: { computed: {
editLink() { editLink() {
return this.release.Links?.editUrl; return this.release._links?.editUrl;
}, },
selfLink() { selfLink() {
return this.release.Links?.self; return this.release._links?.self;
}, },
}, },
}; };
......
...@@ -994,11 +994,6 @@ $ide-commit-header-height: 48px; ...@@ -994,11 +994,6 @@ $ide-commit-header-height: 48px;
} }
.ide-context-header { .ide-context-header {
.ide-merge-requests-dropdown.dropdown-menu {
width: 385px;
max-height: initial;
}
.avatar-container { .avatar-container {
flex: 0 0 auto; flex: 0 0 auto;
margin-right: 0; margin-right: 0;
......
...@@ -15,6 +15,15 @@ module Resolvers ...@@ -15,6 +15,15 @@ module Resolvers
argument :label_name, GraphQL::STRING_TYPE.to_list_type, argument :label_name, GraphQL::STRING_TYPE.to_list_type,
required: false, required: false,
description: 'Labels applied to this issue' description: 'Labels applied to this issue'
argument :milestone_title, GraphQL::STRING_TYPE.to_list_type,
required: false,
description: 'Milestones applied to this issue'
argument :assignee_username, GraphQL::STRING_TYPE,
required: false,
description: 'Username of a user assigned to the issues'
argument :assignee_id, GraphQL::STRING_TYPE,
required: false,
description: 'ID of a user assigned to the issues, "none" and "any" values supported'
argument :created_before, Types::TimeType, argument :created_before, Types::TimeType,
required: false, required: false,
description: 'Issues created before this date' description: 'Issues created before this date'
......
...@@ -780,7 +780,7 @@ class Project < ApplicationRecord ...@@ -780,7 +780,7 @@ class Project < ApplicationRecord
end end
def repository def repository
@repository ||= Repository.new(full_path, self, disk_path: disk_path) @repository ||= Repository.new(full_path, self, shard: repository_storage, disk_path: disk_path)
end end
def cleanup def cleanup
...@@ -1411,8 +1411,8 @@ class Project < ApplicationRecord ...@@ -1411,8 +1411,8 @@ class Project < ApplicationRecord
# Expires various caches before a project is renamed. # Expires various caches before a project is renamed.
def expire_caches_before_rename(old_path) def expire_caches_before_rename(old_path)
repo = Repository.new(old_path, self) repo = Repository.new(old_path, self, shard: repository_storage)
wiki = Repository.new("#{old_path}.wiki", self) wiki = Repository.new("#{old_path}.wiki", self, shard: repository_storage, repo_type: Gitlab::GlRepository::WIKI)
if repo.exists? if repo.exists?
repo.before_delete repo.before_delete
......
...@@ -170,7 +170,7 @@ class ProjectWiki ...@@ -170,7 +170,7 @@ class ProjectWiki
end end
def repository def repository
@repository ||= Repository.new(full_path, @project, disk_path: disk_path, repo_type: Gitlab::GlRepository::WIKI) @repository ||= Repository.new(full_path, @project, shard: repository_storage, disk_path: disk_path, repo_type: Gitlab::GlRepository::WIKI)
end end
def default_branch def default_branch
......
...@@ -22,7 +22,7 @@ class Repository ...@@ -22,7 +22,7 @@ class Repository
include Gitlab::RepositoryCacheAdapter include Gitlab::RepositoryCacheAdapter
attr_accessor :full_path, :disk_path, :container, :repo_type attr_accessor :full_path, :shard, :disk_path, :container, :repo_type
delegate :ref_name_for_sha, to: :raw_repository delegate :ref_name_for_sha, to: :raw_repository
delegate :bundle_to_disk, to: :raw_repository delegate :bundle_to_disk, to: :raw_repository
...@@ -65,8 +65,9 @@ class Repository ...@@ -65,8 +65,9 @@ class Repository
xcode_config: :xcode_project? xcode_config: :xcode_project?
}.freeze }.freeze
def initialize(full_path, container, disk_path: nil, repo_type: Gitlab::GlRepository::PROJECT) def initialize(full_path, container, shard:, disk_path: nil, repo_type: Gitlab::GlRepository::PROJECT)
@full_path = full_path @full_path = full_path
@shard = shard
@disk_path = disk_path || full_path @disk_path = disk_path || full_path
@container = container @container = container
@commit_cache = {} @commit_cache = {}
...@@ -95,7 +96,7 @@ class Repository ...@@ -95,7 +96,7 @@ class Repository
def path_to_repo def path_to_repo
@path_to_repo ||= @path_to_repo ||=
begin begin
storage = Gitlab.config.repositories.storages[container.repository_storage] storage = Gitlab.config.repositories.storages[shard]
File.expand_path( File.expand_path(
File.join(storage.legacy_disk_path, disk_path + '.git') File.join(storage.legacy_disk_path, disk_path + '.git')
...@@ -1181,7 +1182,7 @@ class Repository ...@@ -1181,7 +1182,7 @@ class Repository
end end
def initialize_raw_repository def initialize_raw_repository
Gitlab::Git::Repository.new(container.repository_storage, Gitlab::Git::Repository.new(shard,
disk_path + '.git', disk_path + '.git',
repo_type.identifier_for_container(container), repo_type.identifier_for_container(container),
container.full_path) container.full_path)
......
...@@ -261,7 +261,7 @@ class Snippet < ApplicationRecord ...@@ -261,7 +261,7 @@ class Snippet < ApplicationRecord
end end
def repository def repository
@repository ||= Repository.new(full_path, self, disk_path: disk_path, repo_type: Gitlab::GlRepository::SNIPPET) @repository ||= Repository.new(full_path, self, shard: repository_storage, disk_path: disk_path, repo_type: Gitlab::GlRepository::SNIPPET)
end end
def storage def storage
......
---
title: Search issues in GraphQL API by milestone title and assignees
merge_request: 25794
author:
type: added
...@@ -5403,6 +5403,16 @@ type Project { ...@@ -5403,6 +5403,16 @@ type Project {
A single issue of the project A single issue of the project
""" """
issue( issue(
"""
ID of a user assigned to the issues, "none" and "any" values supported
"""
assigneeId: String
"""
Username of a user assigned to the issues
"""
assigneeUsername: String
""" """
Issues closed after this date Issues closed after this date
""" """
...@@ -5438,6 +5448,11 @@ type Project { ...@@ -5438,6 +5448,11 @@ type Project {
""" """
labelName: [String] labelName: [String]
"""
Milestones applied to this issue
"""
milestoneTitle: [String]
""" """
Search query for finding issues by title or description Search query for finding issues by title or description
""" """
...@@ -5473,6 +5488,16 @@ type Project { ...@@ -5473,6 +5488,16 @@ type Project {
""" """
after: String after: String
"""
ID of a user assigned to the issues, "none" and "any" values supported
"""
assigneeId: String
"""
Username of a user assigned to the issues
"""
assigneeUsername: String
""" """
Returns the elements in the list that come before the specified cursor. Returns the elements in the list that come before the specified cursor.
""" """
...@@ -5523,6 +5548,11 @@ type Project { ...@@ -5523,6 +5548,11 @@ type Project {
""" """
last: Int last: Int
"""
Milestones applied to this issue
"""
milestoneTitle: [String]
""" """
Search query for finding issues by title or description Search query for finding issues by title or description
""" """
......
...@@ -749,6 +749,40 @@ ...@@ -749,6 +749,40 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "milestoneTitle",
"description": "Milestones applied to this issue",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "assigneeUsername",
"description": "Username of a user assigned to the issues",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "assigneeId",
"description": "ID of a user assigned to the issues, \"none\" and \"any\" values supported",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{ {
"name": "createdBefore", "name": "createdBefore",
"description": "Issues created before this date", "description": "Issues created before this date",
...@@ -894,6 +928,40 @@ ...@@ -894,6 +928,40 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "milestoneTitle",
"description": "Milestones applied to this issue",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "assigneeUsername",
"description": "Username of a user assigned to the issues",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "assigneeId",
"description": "ID of a user assigned to the issues, \"none\" and \"any\" values supported",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{ {
"name": "createdBefore", "name": "createdBefore",
"description": "Issues created before this date", "description": "Issues created before this date",
......
# Group Import/Export API # Group Import/Export API
> Introduced in GitLab 12.8 as an experimental feature. May change in future releases. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20353) in GitLab 12.8 as an experimental feature. May change in future releases.
Group Import/Export functionality allows to export group structure and import it at a new location. Group Import/Export allows you to export group structure and import it to a new location.
Used in combination with [Project Import/Export](project_import_export.md) it allows you to preserve connections with group level relations When used with [Project Import/Export](project_import_export.md), you can preserve connections with
(e.g. a connection between a project issue and group epic). group-level relationships, such as connections between project issues and group epics.
Group Export includes: Group exports include the following:
1. Group Milestones - Group milestones
1. Group Boards - Group boards
1. Group Labels - Group labels
1. Group Badges - Group badges
1. Group Members - Group members
1. Sub-groups (each sub-group includes all data above) - Sub-groups. Each sub-group includes all data above
## Schedule new export ## Schedule new export
...@@ -58,7 +58,11 @@ ls *export.tar.gz ...@@ -58,7 +58,11 @@ ls *export.tar.gz
2020-12-05_22-11-148_namespace_export.tar.gz 2020-12-05_22-11-148_namespace_export.tar.gz
``` ```
Time spent on exporting a group may vary depending on a size of the group. Export download endpoint will return exported archive once it is available. 404 is returned otherwise. Time spent on exporting a group may vary depending on a size of the group. This endpoint
returns either:
- The exported archive (when available)
- A 404 message
## Import a file ## Import a file
...@@ -81,3 +85,12 @@ by `@`. For example: ...@@ -81,3 +85,12 @@ by `@`. For example:
```shell ```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "name=imported-group" --form "path=imported-group" --form "file=@/path/to/file" https://gitlab.example.com/api/v4/groups/import curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "name=imported-group" --form "path=imported-group" --form "file=@/path/to/file" https://gitlab.example.com/api/v4/groups/import
``` ```
## Important notes
Note the following:
- To preserve group-level relationships from imported projects, run Group Import/Export first,
to allow project imports into the desired group structure.
- Imported groups are given a `private` visibility level, unless imported into a parent group.
- If imported into a parent group, subgroups will inherit a similar level of visibility, unless otherwise restricted.
...@@ -22,7 +22,7 @@ description: 'Learn how to contribute to GitLab.' ...@@ -22,7 +22,7 @@ description: 'Learn how to contribute to GitLab.'
- [Code review guidelines](code_review.md) for reviewing code and having code reviewed - [Code review guidelines](code_review.md) for reviewing code and having code reviewed
- [Database review guidelines](database_review.md) for reviewing database-related changes and complex SQL queries, and having them reviewed - [Database review guidelines](database_review.md) for reviewing database-related changes and complex SQL queries, and having them reviewed
- [Secure coding guidlines](https://gitlab.com/gitlab-com/gl-security/security-guidelines) - [Secure coding guidelines](https://gitlab.com/gitlab-com/gl-security/security-guidelines)
- [Pipelines for the GitLab project](pipelines.md) - [Pipelines for the GitLab project](pipelines.md)
Complementary reads: Complementary reads:
......
...@@ -222,6 +222,7 @@ requirements. ...@@ -222,6 +222,7 @@ requirements.
1. Regressions and bugs are covered with tests that reduce the risk of the issue happening 1. Regressions and bugs are covered with tests that reduce the risk of the issue happening
again. again.
1. [Performance guidelines](../merge_request_performance_guidelines.md) have been followed. 1. [Performance guidelines](../merge_request_performance_guidelines.md) have been followed.
1. [Secure coding guidelines](https://gitlab.com/gitlab-com/gl-security/security-guidelines) have been followed.
1. [Documented](../documentation/index.md) in the `/doc` directory. 1. [Documented](../documentation/index.md) in the `/doc` directory.
1. [Changelog entry added](../changelog.md), if necessary. 1. [Changelog entry added](../changelog.md), if necessary.
1. Reviewed by relevant (UX/FE/BE/tech writing) reviewers and all concerns are addressed. 1. Reviewed by relevant (UX/FE/BE/tech writing) reviewers and all concerns are addressed.
......
...@@ -12,6 +12,7 @@ See also: ...@@ -12,6 +12,7 @@ See also:
- [Project import/export API](../../../api/project_import_export.md) - [Project import/export API](../../../api/project_import_export.md)
- [Project import/export administration rake tasks](../../../administration/raketasks/project_import_export.md) **(CORE ONLY)** - [Project import/export administration rake tasks](../../../administration/raketasks/project_import_export.md) **(CORE ONLY)**
- [Group import/export API](../../../api/group_import_export.md)
To set up a project import/export: To set up a project import/export:
......
...@@ -94,8 +94,27 @@ describe('text_utility', () => { ...@@ -94,8 +94,27 @@ describe('text_utility', () => {
}); });
describe('convertToCamelCase', () => { describe('convertToCamelCase', () => {
it('converts snake_case string to camelCase string', () => { it.each`
expect(textUtils.convertToCamelCase('snake_case')).toBe('snakeCase'); txt | result
${'a_snake_cased_string'} | ${'aSnakeCasedString'}
${'_leading_underscore'} | ${'_leadingUnderscore'}
${'__leading_underscores'} | ${'__leadingUnderscores'}
${'trailing_underscore_'} | ${'trailingUnderscore_'}
${'trailing_underscores__'} | ${'trailingUnderscores__'}
`('converts string "$txt" to "$result"', ({ txt, result }) => {
expect(textUtils.convertToCamelCase(txt)).toBe(result);
});
it.each`
txt
${'__withoutMiddleUnderscores__'}
${''}
${'with spaces'}
${'with\nnew\r\nlines'}
${'_'}
${'___'}
`('does not modify string "$txt"', ({ txt }) => {
expect(textUtils.convertToCamelCase(txt)).toBe(txt);
}); });
}); });
......
...@@ -37,13 +37,13 @@ describe('Release block header', () => { ...@@ -37,13 +37,13 @@ describe('Release block header', () => {
const link = findHeaderLink(); const link = findHeaderLink();
expect(link.text()).toBe(release.name); expect(link.text()).toBe(release.name);
expect(link.attributes('href')).toBe(release.Links.self); expect(link.attributes('href')).toBe(release._links.self);
}); });
}); });
describe('when _links.self is missing', () => { describe('when _links.self is missing', () => {
beforeEach(() => { beforeEach(() => {
factory({ Links: { self: null } }); factory({ _links: { self: null } });
}); });
it('renders the title as text', () => { it('renders the title as text', () => {
......
...@@ -63,7 +63,7 @@ describe('Release block', () => { ...@@ -63,7 +63,7 @@ describe('Release block', () => {
it('renders an edit button that links to the "Edit release" page', () => { it('renders an edit button that links to the "Edit release" page', () => {
expect(editButton().exists()).toBe(true); expect(editButton().exists()).toBe(true);
expect(editButton().attributes('href')).toBe(release.Links.editUrl); expect(editButton().attributes('href')).toBe(release._links.editUrl);
}); });
it('renders release name', () => { it('renders release name', () => {
...@@ -150,8 +150,8 @@ describe('Release block', () => { ...@@ -150,8 +150,8 @@ describe('Release block', () => {
}); });
}); });
it("does not render an edit button if release.Links.editUrl isn't a string", () => { it("does not render an edit button if release._links.editUrl isn't a string", () => {
delete release.Links; delete release._links;
return factory(release).then(() => { return factory(release).then(() => {
expect(editButton().exists()).toBe(false); expect(editButton().exists()).toBe(false);
......
...@@ -8,11 +8,13 @@ describe Resolvers::IssuesResolver do ...@@ -8,11 +8,13 @@ describe Resolvers::IssuesResolver do
let(:current_user) { create(:user) } let(:current_user) { create(:user) }
context "with a project" do context "with a project" do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:issue1) { create(:issue, project: project, state: :opened, created_at: 3.hours.ago, updated_at: 3.hours.ago) } let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:issue2) { create(:issue, project: project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago) } let_it_be(:assignee) { create(:user) }
let_it_be(:label1) { create(:label, project: project) } let_it_be(:issue1) { create(:issue, project: project, state: :opened, created_at: 3.hours.ago, updated_at: 3.hours.ago, milestone: milestone) }
let_it_be(:label2) { create(:label, project: project) } let_it_be(:issue2) { create(:issue, project: project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago, assignees: [assignee]) }
let_it_be(:label1) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) }
before do before do
project.add_developer(current_user) project.add_developer(current_user)
...@@ -31,6 +33,26 @@ describe Resolvers::IssuesResolver do ...@@ -31,6 +33,26 @@ describe Resolvers::IssuesResolver do
expect(resolve_issues(state: 'closed')).to contain_exactly(issue2) expect(resolve_issues(state: 'closed')).to contain_exactly(issue2)
end end
it 'filters by milestone' do
expect(resolve_issues(milestone_title: milestone.title)).to contain_exactly(issue1)
end
it 'filters by assignee_username' do
expect(resolve_issues(assignee_username: assignee.username)).to contain_exactly(issue2)
end
it 'filters by assignee_id' do
expect(resolve_issues(assignee_id: assignee.id)).to contain_exactly(issue2)
end
it 'filters by any assignee' do
expect(resolve_issues(assignee_id: IssuableFinder::FILTER_ANY)).to contain_exactly(issue2)
end
it 'filters by no assignee' do
expect(resolve_issues(assignee_id: IssuableFinder::FILTER_NONE)).to contain_exactly(issue1)
end
it 'filters by labels' do it 'filters by labels' do
expect(resolve_issues(label_name: [label1.title])).to contain_exactly(issue1, issue2) expect(resolve_issues(label_name: [label1.title])).to contain_exactly(issue1, issue2)
expect(resolve_issues(label_name: [label1.title, label2.title])).to contain_exactly(issue2) expect(resolve_issues(label_name: [label1.title, label2.title])).to contain_exactly(issue2)
......
...@@ -1791,21 +1791,19 @@ describe Project do ...@@ -1791,21 +1791,19 @@ describe Project do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:repo) { double(:repo, exists?: true) } let(:repo) { double(:repo, exists?: true) }
let(:wiki) { double(:wiki, exists?: true) } let(:wiki) { double(:wiki, exists?: true) }
let(:design) { double(:wiki, exists?: false) }
it 'expires the caches of the repository and wiki' do it 'expires the caches of the repository and wiki' do
# In EE, there are design repositories as well
allow(Repository).to receive(:new).and_call_original
allow(Repository).to receive(:new) allow(Repository).to receive(:new)
.with('foo', project) .with('foo', project, shard: project.repository_storage)
.and_return(repo) .and_return(repo)
allow(Repository).to receive(:new) allow(Repository).to receive(:new)
.with('foo.wiki', project) .with('foo.wiki', project, shard: project.repository_storage, repo_type: Gitlab::GlRepository::WIKI)
.and_return(wiki) .and_return(wiki)
allow(Repository).to receive(:new)
.with('foo.design', project)
.and_return(design)
expect(repo).to receive(:before_delete) expect(repo).to receive(:before_delete)
expect(wiki).to receive(:before_delete) expect(wiki).to receive(:before_delete)
......
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