summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml4
-rw-r--r--CHANGELOG35
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock6
-rw-r--r--app/assets/javascripts/api.js9
-rw-r--r--app/assets/javascripts/boards/components/board.js.es68
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es69
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.js.es658
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js.es63
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js.es62
-rw-r--r--app/assets/javascripts/boards/models/list.js.es611
-rw-r--r--app/assets/javascripts/boards/services/board_service.js.es66
-rw-r--r--app/assets/javascripts/create_label.js.es69
-rw-r--r--app/assets/javascripts/labels_select.js7
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js5
-rw-r--r--app/assets/javascripts/merge_request_tabs.js23
-rw-r--r--app/assets/javascripts/pipeline.js.es611
-rw-r--r--app/assets/javascripts/user_tabs.js.es611
-rw-r--r--app/assets/javascripts/users_select.js4
-rw-r--r--app/assets/stylesheets/framework/buttons.scss9
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss17
-rw-r--r--app/assets/stylesheets/framework/flash.scss9
-rw-r--r--app/assets/stylesheets/framework/forms.scss4
-rw-r--r--app/assets/stylesheets/framework/header.scss4
-rw-r--r--app/assets/stylesheets/framework/selects.scss9
-rw-r--r--app/assets/stylesheets/pages/boards.scss32
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss15
-rw-r--r--app/assets/stylesheets/pages/profile.scss25
-rw-r--r--app/controllers/projects/boards/issues_controller.rb37
-rw-r--r--app/controllers/projects/issues_controller.rb3
-rw-r--r--app/controllers/projects/labels_controller.rb10
-rw-r--r--app/controllers/projects/merge_requests_controller.rb66
-rw-r--r--app/models/ci/pipeline.rb3
-rw-r--r--app/models/commit_status.rb17
-rw-r--r--app/models/merge_request.rb4
-rw-r--r--app/models/repository.rb46
-rw-r--r--app/services/base_service.rb7
-rw-r--r--app/services/boards/issues/create_service.rb16
-rw-r--r--app/services/boards/issues/list_service.rb7
-rw-r--r--app/services/files/base_service.rb11
-rw-r--r--app/services/files/multi_service.rb124
-rw-r--r--app/services/files/update_service.rb6
-rw-r--r--app/services/projects/create_service.rb12
-rw-r--r--app/services/projects/fork_service.rb2
-rw-r--r--app/views/ci/lints/_create.html.haml3
-rw-r--r--app/views/dashboard/todos/index.html.haml2
-rw-r--r--app/views/explore/groups/index.html.haml2
-rw-r--r--app/views/explore/projects/_filter.html.haml4
-rw-r--r--app/views/groups/milestones/new.html.haml4
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/projects/boards/components/_board.html.haml41
-rw-r--r--app/views/projects/boards/components/_card.html.haml6
-rw-r--r--app/views/projects/branches/index.html.haml2
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml4
-rw-r--r--app/views/projects/commit/_commit_box.html.haml4
-rw-r--r--app/views/projects/commit/_pipeline.html.haml2
-rw-r--r--app/views/projects/deployments/_actions.haml2
-rw-r--r--app/views/projects/diffs/_diffs.html.haml3
-rw-r--r--app/views/projects/edit.html.haml3
-rw-r--r--app/views/projects/forks/index.html.haml2
-rw-r--r--app/views/projects/group_links/index.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml2
-rw-r--r--app/views/projects/labels/_label.html.haml2
-rw-r--r--app/views/projects/merge_requests/_new_diffs.html.haml1
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml28
-rw-r--r--app/views/projects/merge_requests/_show.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_mr_title.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_versions.html.haml4
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml5
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml2
-rw-r--r--app/views/projects/snippets/_actions.html.haml2
-rw-r--r--app/views/projects/tags/index.html.haml2
-rw-r--r--app/views/shared/_new_project_item_select.html.haml2
-rw-r--r--app/views/shared/_sort_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_filter.html.haml2
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/notifications/_button.html.haml2
-rw-r--r--app/views/snippets/_actions.html.haml2
-rw-r--r--app/views/users/show.html.haml6
-rw-r--r--app/workers/process_pipeline_worker.rb10
-rw-r--r--app/workers/update_pipeline_worker.rb10
-rw-r--r--config/application.rb2
-rw-r--r--config/initializers/sentry.rb2
-rw-r--r--config/routes/group.rb8
-rw-r--r--config/routes/project.rb3
-rw-r--r--config/routes/user.rb35
-rw-r--r--db/fixtures/development/14_pipelines.rb2
-rw-r--r--doc/README.md2
-rw-r--r--doc/administration/container_registry.md96
-rw-r--r--doc/api/boards.md251
-rw-r--r--doc/api/commits.md87
-rw-r--r--doc/ci/yaml/README.md34
-rw-r--r--doc/container_registry/README.md99
-rw-r--r--doc/container_registry/img/container_registry.pngbin222782 -> 0 bytes
-rw-r--r--doc/container_registry/img/project_feature.pngbin248750 -> 0 bytes
-rw-r--r--doc/container_registry/troubleshooting.md142
-rw-r--r--doc/development/licensing.md2
-rw-r--r--doc/install/installation.md2
-rw-r--r--doc/update/8.12-to-8.13.md2
-rw-r--r--doc/user/project/container_registry.md253
-rw-r--r--doc/user/project/cycle_analytics.md88
-rw-r--r--doc/user/project/img/container_registry_enable.pngbin0 -> 5526 bytes
-rw-r--r--doc/user/project/img/container_registry_panel.pngbin0 -> 96315 bytes
-rw-r--r--doc/user/project/img/container_registry_tab.pngbin0 -> 7284 bytes
-rw-r--r--doc/user/project/img/cycle_analytics_landing_page.pngbin58203 -> 66080 bytes
-rw-r--r--doc/user/project/img/mitmproxy-docker.png (renamed from doc/container_registry/img/mitmproxy-docker.png)bin407004 -> 407004 bytes
-rw-r--r--doc/user/project/issue_board.md7
-rw-r--r--features/project/commits/commits.feature5
-rw-r--r--features/project/source/browse_files.feature17
-rw-r--r--features/steps/project/commits/commits.rb8
-rw-r--r--features/steps/project/fork.rb1
-rw-r--r--features/steps/project/source/browse_files.rb4
-rw-r--r--lib/api/access_requests.rb64
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/boards.rb115
-rw-r--r--lib/api/commits.rb36
-rw-r--r--lib/api/entities.rb18
-rw-r--r--lib/api/helpers.rb5
-rw-r--r--lib/api/members.rb96
-rw-r--r--lib/constraints/group_url_constrainer.rb7
-rw-r--r--lib/constraints/namespace_url_constrainer.rb13
-rw-r--r--lib/constraints/user_url_constrainer.rb7
-rw-r--r--lib/gitlab/github_import/client.rb14
-rw-r--r--lib/gitlab/import_export/attribute_cleaner.rb13
-rw-r--r--lib/gitlab/import_export/command_line_util.rb9
-rw-r--r--lib/gitlab/import_export/file_importer.rb2
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb5
-rw-r--r--lib/gitlab/import_export/project_tree_saver.rb4
-rw-r--r--lib/gitlab/import_export/relation_factory.rb12
-rw-r--r--lib/gitlab/import_export/repo_restorer.rb2
-rw-r--r--lib/gitlab/import_export/repo_saver.rb2
-rw-r--r--lib/gitlab/import_export/version_saver.rb4
-rw-r--r--lib/gitlab/import_export/wiki_repo_saver.rb2
-rw-r--r--lib/gitlab/redis.rb12
-rw-r--r--spec/controllers/projects/boards/issues_controller_spec.rb64
-rw-r--r--spec/controllers/users_controller_spec.rb4
-rw-r--r--spec/features/boards/new_issue_spec.rb80
-rw-r--r--spec/features/projects/badges/coverage_spec.rb2
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb2
-rw-r--r--spec/features/users_spec.rb11
-rw-r--r--spec/helpers/projects_helper_spec.rb4
-rw-r--r--spec/lib/constraints/group_url_constrainer_spec.rb10
-rw-r--r--spec/lib/constraints/namespace_url_constrainer_spec.rb25
-rw-r--r--spec/lib/constraints/user_url_constrainer_spec.rb10
-rw-r--r--spec/lib/gitlab/badge/coverage/report_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/attribute_cleaner_spec.rb34
-rw-r--r--spec/lib/gitlab/import_export/relation_factory_spec.rb125
-rw-r--r--spec/lib/gitlab/redis_spec.rb34
-rw-r--r--spec/models/forked_project_link_spec.rb1
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb2
-rw-r--r--spec/requests/api/api_helpers_spec.rb39
-rw-r--r--spec/requests/api/boards_spec.rb192
-rw-r--r--spec/requests/api/commits_spec.rb273
-rw-r--r--spec/requests/api/fork_spec.rb2
-rw-r--r--spec/requests/api/members_spec.rb16
-rw-r--r--spec/routing/routing_spec.rb4
-rw-r--r--spec/services/boards/issues/create_service_spec.rb33
-rw-r--r--spec/services/boards/issues/list_service_spec.rb8
-rw-r--r--spec/services/files/update_service_spec.rb4
-rw-r--r--spec/services/projects/fork_service_spec.rb25
-rw-r--r--spec/services/system_note_service_spec.rb4
-rw-r--r--spec/support/import_export/export_file_helper.rb4
-rw-r--r--spec/views/ci/lints/show.html.haml_spec.rb46
-rw-r--r--spec/workers/process_pipeline_worker_spec.rb22
-rw-r--r--spec/workers/update_pipeline_worker_spec.rb22
167 files changed, 2898 insertions, 701 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5d2fad03f19..8645488335e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -207,9 +207,7 @@ rubocop: *exec
rake haml_lint: *exec
rake scss_lint: *exec
rake brakeman: *exec
-rake flay:
- <<: *exec
- allow_failure: yes
+rake flay: *exec
license_finder: *exec
rake downtime_check: *exec
diff --git a/CHANGELOG b/CHANGELOG
index fbafda4d9ad..c20def1c092 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -3,22 +3,29 @@ Please view this file on the master branch, on stable branches it's out of date.
v 8.13.0 (unreleased)
- Update runner version only when updating contacted_at
- Add link from system note to compare with previous version
+ - Improve issue load time performance by avoiding ORDER BY in find_by call
- Use gitlab-shell v3.6.2 (GIT TRACE logging)
- Fix centering of custom header logos (Ashley Dumaine)
- AbstractReferenceFilter caches project_refs on RequestStore when active
- Replaced the check sign to arrow in the show build view. !6501
- Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar)
- Speed-up group milestones show page
+ - Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs)
+ - Add tag shortcut from the Commit page. !6543
- Keep refs for each deployment
- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
- Add more tests for calendar contribution (ClemMakesApps)
- Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
- Simplify Mentionable concern instance methods
- Fix permission for setting an issue's due date
+ - API: Multi-file commit !6096 (mahcsig)
+ - Revert "Label list shows all issues (opened or closed) with that label"
- Expose expires_at field when sharing project on API
- Fix VueJS template tags being rendered in code comments
- Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
+ - Add Issue Board API support (andrebsguedes)
- Allow the Koding integration to be configured through the API
+ - Add new issue button to each list on Issues Board
- Added soft wrap button to repository file/blob editor
- Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
- Fix todos page mobile viewport layout (ClemMakesApps)
@@ -31,12 +38,15 @@ v 8.13.0 (unreleased)
- Replace `alias_method_chain` with `Module#prepend`
- Enable GitLab Import/Export for non-admin users.
- Preserve label filters when sorting !6136 (Joseph Frazier)
+ - MergeRequest#new form load diff asynchronously
- Only update issuable labels if they have been changed
- Take filters in account in issuable counters. !6496
- Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*)
+ - Prevent flash alert text from being obscured when container is fluid
- Append issue template to existing description !6149 (Joseph Frazier)
- Trending projects now only show public projects and the list of projects is cached for a day
- Revoke button in Applications Settings underlines on hover.
+ - Use higher size on Gitlab::Redis connection pool on Sidekiq servers
- Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
- Fix Long commit messages overflow viewport in file tree
- Revert avoid touching file system on Build#artifacts?
@@ -44,8 +54,10 @@ v 8.13.0 (unreleased)
- Add broadcast messages and alerts below sub-nav
- Better empty state for Groups view
- Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe)
+ - Replace bootstrap caret with fontawesome caret (ClemMakesApps)
- Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
- Add organization field to user profile
+ - Fix deploy status responsiveness error !6633
- Fix resolved discussion display in side-by-side diff view !6575
- Optimize GitHub importing for speed and memory
- API: expose pipeline data in builds API (!6502, Guilherme Salazar)
@@ -59,13 +71,22 @@ v 8.13.0 (unreleased)
- Add Container Registry on/off status to Admin Area !6638 (the-undefined)
- Grouped pipeline dropdown is a scrollable container
-v 8.12.4 (unreleased)
- - Fix type mismatch bug when closing Jira issue
- - Skip wiki creation when GitHub project has wiki enabled
- - Fix failed project deletion when feature visibility set to private
- - Fix issues importing services via Import/Export
- - Restrict failed login attempts for users with 2FA enabled
- - Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. (lukehowell)
+v 8.12.5 (unreleased)
+
+v 8.12.4
+ - Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. !6294 (lukehowell)
+ - Fix padding in build sidebar. !6506
+ - Changed compare dropdowns to dropdowns with isolated search input. !6550
+ - Fix race condition on LFS Token. !6592
+ - Fix type mismatch bug when closing Jira issue. !6619
+ - Fix lint-doc error. !6623
+ - Skip wiki creation when GitHub project has wiki enabled. !6665
+ - Fix issues importing services via Import/Export. !6667
+ - Restrict failed login attempts for users with 2FA enabled. !6668
+ - Fix failed project deletion when feature visibility set to private. !6688
+ - Prevent claiming associated model IDs via import.
+ - Set GitLab project exported file permissions to owner only
+ - Change user & group landing page routing from /u/:username to /:username
v 8.12.3
- Update Gitlab Shell to support low IO priority for storage moves
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 100435be135..b60d71966ae 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-0.8.2
+0.8.4
diff --git a/Gemfile b/Gemfile
index 18654a1e88c..3e8ce8b2fc5 100644
--- a/Gemfile
+++ b/Gemfile
@@ -231,7 +231,7 @@ gem 'net-ssh', '~> 3.0.1'
gem 'base32', '~> 0.3.0'
# Sentry integration
-gem 'sentry-raven', '~> 1.1.0'
+gem 'sentry-raven', '~> 2.0.0'
gem 'premailer-rails', '~> 1.9.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 3f756fec929..96b49faf727 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -665,8 +665,8 @@ GEM
activesupport (>= 3.1)
select2-rails (3.5.9.3)
thor (~> 0.14)
- sentry-raven (1.1.0)
- faraday (>= 0.7.6)
+ sentry-raven (2.0.2)
+ faraday (>= 0.7.6, < 0.10.x)
settingslogic (2.0.9)
sexp_processor (4.7.0)
sham_rack (1.3.6)
@@ -948,7 +948,7 @@ DEPENDENCIES
sdoc (~> 0.3.20)
seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9)
- sentry-raven (~> 1.1.0)
+ sentry-raven (~> 2.0.0)
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0)
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 4f04392513f..599331df3f5 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -5,7 +5,7 @@
namespacesPath: "/api/:version/namespaces.json",
groupProjectsPath: "/api/:version/groups/:id/projects.json",
projectsPath: "/api/:version/projects.json?simple=true",
- labelsPath: "/api/:version/projects/:id/labels",
+ labelsPath: "/:namespace_path/:project_path/labels",
licensePath: "/api/:version/licenses/:key",
gitignorePath: "/api/:version/gitignores/:key",
gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key",
@@ -66,13 +66,14 @@
return callback(projects);
});
},
- newLabel: function(project_id, data, callback) {
+ newLabel: function(namespace_path, project_path, data, callback) {
var url = Api.buildUrl(Api.labelsPath)
- .replace(':id', project_id);
+ .replace(':namespace_path', namespace_path)
+ .replace(':project_path', project_path);
return $.ajax({
url: url,
type: "POST",
- data: data,
+ data: {'label': data},
dataType: "json"
}).done(function(label) {
return callback(label);
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
index 7e86f001f44..cacb36a897f 100644
--- a/app/assets/javascripts/boards/components/board.js.es6
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -21,7 +21,8 @@
},
data () {
return {
- filters: Store.state.filters
+ filters: Store.state.filters,
+ showIssueForm: false
};
},
watch: {
@@ -33,6 +34,11 @@
deep: true
}
},
+ methods: {
+ showNewIssueForm() {
+ this.showIssueForm = !this.showIssueForm;
+ }
+ },
ready () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled,
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
index 474805c1437..7022a29e818 100644
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -1,4 +1,5 @@
//= require ./board_card
+//= require ./board_new_issue
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -8,14 +9,16 @@
gl.issueBoards.BoardList = Vue.extend({
components: {
- 'board-card': gl.issueBoards.BoardCard
+ 'board-card': gl.issueBoards.BoardCard,
+ 'board-new-issue': gl.issueBoards.BoardNewIssue
},
props: {
disabled: Boolean,
list: Object,
issues: Array,
loading: Boolean,
- issueLinkBase: String
+ issueLinkBase: String,
+ showIssueForm: Boolean
},
data () {
return {
@@ -73,7 +76,7 @@
group: 'issues',
sort: false,
disabled: this.disabled,
- filter: '.board-list-count',
+ filter: '.board-list-count, .is-disabled',
onStart: (e) => {
const card = this.$refs.issue[e.oldIndex];
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6
new file mode 100644
index 00000000000..a4fad422eca
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6
@@ -0,0 +1,58 @@
+(() => {
+ window.gl = window.gl || {};
+
+ gl.issueBoards.BoardNewIssue = Vue.extend({
+ props: {
+ list: Object,
+ showIssueForm: Boolean
+ },
+ data() {
+ return {
+ title: '',
+ error: false
+ };
+ },
+ watch: {
+ showIssueForm () {
+ this.$els.input.focus();
+ }
+ },
+ methods: {
+ submit(e) {
+ e.preventDefault();
+ if (this.title.trim() === '') return;
+
+ this.error = false;
+
+ const labels = this.list.label ? [this.list.label] : [];
+ const issue = new ListIssue({
+ title: this.title,
+ labels
+ });
+
+ this.list.newIssue(issue)
+ .then((data) => {
+ // Need this because our jQuery very kindly disables buttons on ALL form submissions
+ $(this.$els.submitButton).enable();
+ })
+ .catch(() => {
+ // Need this because our jQuery very kindly disables buttons on ALL form submissions
+ $(this.$els.submitButton).enable();
+
+ // Remove the issue
+ this.list.removeIssue(issue);
+
+ // Show error message
+ this.error = true;
+ this.showIssueForm = true;
+ });
+
+ this.cancel();
+ },
+ cancel() {
+ this.showIssueForm = false;
+ this.title = '';
+ }
+ }
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
index 1a4d8157970..6ccd83e2d84 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
@@ -3,8 +3,7 @@ $(() => {
$('.js-new-board-list').each(function () {
const $this = $(this);
-
- new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('project-id'));
+ new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
$this.glDropdown({
data(term, callback) {
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
index 44addb3ea98..f629d45c587 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
@@ -21,7 +21,7 @@
fallbackClass: 'is-dragging',
fallbackOnBody: true,
ghostClass: 'is-ghost',
- filter: '.has-tooltip',
+ filter: '.has-tooltip, .btn',
delay: gl.issueBoards.touchEnabled ? 100 : 0,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20,
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6
index 91fd620fdb3..5d0a561cdba 100644
--- a/app/assets/javascripts/boards/models/list.js.es6
+++ b/app/assets/javascripts/boards/models/list.js.es6
@@ -87,6 +87,17 @@ class List {
});
}
+ newIssue (issue) {
+ this.addIssue(issue);
+ this.issuesSize++;
+
+ return gl.boardService.newIssue(this.id, issue)
+ .then((resp) => {
+ const data = resp.json();
+ issue.id = data.iid;
+ });
+ }
+
createIssues (data) {
data.forEach((issueObj) => {
this.addIssue(new ListIssue(issueObj));
diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6
index 9b80fb2e99f..2b825c3949f 100644
--- a/app/assets/javascripts/boards/services/board_service.js.es6
+++ b/app/assets/javascripts/boards/services/board_service.js.es6
@@ -58,4 +58,10 @@ class BoardService {
to_list_id
});
}
+
+ newIssue (id, issue) {
+ return this.issues.save({ id }, {
+ issue
+ });
+ }
};
diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6
index 46d1c3f00c1..c5f8c29242d 100644
--- a/app/assets/javascripts/create_label.js.es6
+++ b/app/assets/javascripts/create_label.js.es6
@@ -1,8 +1,9 @@
(function (w) {
class CreateLabelDropdown {
- constructor ($el, projectId) {
+ constructor ($el, namespacePath, projectPath) {
this.$el = $el;
- this.projectId = projectId;
+ this.namespacePath = namespacePath;
+ this.projectPath = projectPath;
this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown'));
this.$cancelButton = $('.js-cancel-label-btn', this.$el);
this.$newLabelField = $('#new_label_name', this.$el);
@@ -91,8 +92,8 @@
e.preventDefault();
e.stopPropagation();
- Api.newLabel(this.projectId, {
- name: this.$newLabelField.val(),
+ Api.newLabel(this.namespacePath, this.projectPath, {
+ title: this.$newLabelField.val(),
color: this.$newColorField.val()
}, (label) => {
this.$newLabelCreateButton.enable();
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 69d9e0a4d79..e356872624a 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -4,10 +4,11 @@
var _this;
_this = this;
$('.js-label-select').each(function(i, dropdown) {
- var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove;
+ var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove;
$dropdown = $(dropdown);
$toggleText = $dropdown.find('.dropdown-toggle-text');
- projectId = $dropdown.data('project-id');
+ namespacePath = $dropdown.data('namespace-path');
+ projectPath = $dropdown.data('project-path');
labelUrl = $dropdown.data('labels');
issueUpdateURL = $dropdown.data('issueUpdate');
selectedLabel = $dropdown.data('selected');
@@ -45,7 +46,7 @@
$sidebarLabelTooltip.tooltip();
if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
- new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId);
+ new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath);
}
saveLabelData = function() {
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 9299d0eabd2..b170e26eebf 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -38,6 +38,11 @@
gl.utils.getPagePath = function() {
return $('body').data('page').split(':')[0];
};
+ gl.utils.parseUrl = function (url) {
+ var parser = document.createElement('a');
+ parser.href = url;
+ return parser;
+ };
return jQuery.timefor = function(time, suffix, expiredLabel) {
var suffixFromNow, timefor;
if (!time) {
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index bec11a198a1..8045d24a1bb 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -61,6 +61,9 @@
function MergeRequestTabs(opts) {
this.opts = opts != null ? opts : {};
this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
+
+ this.buildsLoaded = this.opts.buildsLoaded || false;
+
this.setCurrentAction = bind(this.setCurrentAction, this);
this.tabShown = bind(this.tabShown, this);
this.showTab = bind(this.showTab, this);
@@ -93,7 +96,7 @@
this.loadCommits($target.attr('href'));
this.expandView();
this.resetViewContainer();
- } else if (action === 'diffs') {
+ } else if (this.isDiffAction(action)) {
this.loadDiff($target.attr('href'));
if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') {
this.shrinkView();
@@ -170,8 +173,9 @@
action = 'notes';
}
this.currentAction = action;
- // Remove a trailing '/commits' or '/diffs'
- new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, '');
+ // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs'
+ new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
+
// Append the new action if we're on a tab other than 'notes'
if (action !== 'notes') {
new_state += "/" + action;
@@ -210,8 +214,13 @@
if (this.diffsLoaded) {
return;
}
+
+ // We extract pathname for the current Changes tab anchor href
+ // some pages like MergeRequestsController#new has query parameters on that anchor
+ var url = gl.utils.parseUrl(source);
+
return this._get({
- url: (source + ".json") + this._location.search,
+ url: (url.pathname + ".json") + this._location.search,
success: (function(_this) {
return function(data) {
$('#diffs').html(data.html);
@@ -223,7 +232,7 @@
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
$('#diffs .js-syntax-highlight').syntaxHighlight();
$('#diffs .diff-file').singleFileDiff();
- if (_this.diffViewType() === 'parallel' && _this.currentAction === 'diffs') {
+ if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) {
_this.expandViewContainer();
}
_this.diffsLoaded = true;
@@ -324,6 +333,10 @@
return $('.inline-parallel-buttons a.active').data('view-type');
};
+ MergeRequestTabs.prototype.isDiffAction = function(action) {
+ return action === 'diffs' || action === 'new/diffs'
+ };
+
MergeRequestTabs.prototype.expandViewContainer = function() {
var $wrapper = $('.content-wrapper .container-fluid');
if (this.fixedLayoutPref === null) {
diff --git a/app/assets/javascripts/pipeline.js.es6 b/app/assets/javascripts/pipeline.js.es6
index bf33eb10100..8813bb5dfef 100644
--- a/app/assets/javascripts/pipeline.js.es6
+++ b/app/assets/javascripts/pipeline.js.es6
@@ -3,12 +3,21 @@
const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
const $btnText = $(this).find('.toggle-btn-text');
+ const $icon = $(this).find('.fa');
$($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
+ const expandIcon = 'fa-caret-down';
+ const hideIcon = 'fa-caret-up';
- graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide')
+ if(graphCollapsed) {
+ $btnText.text('Expand');
+ $icon.removeClass(hideIcon).addClass(expandIcon);
+ } else {
+ $btnText.text('Hide');
+ $icon.removeClass(expandIcon).addClass(hideIcon);
+ }
}
$(document).on('click', '.toggle-pipeline-btn', toggleGraph);
diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6
index 63bce0a6f6f..dfdfa1e7f75 100644
--- a/app/assets/javascripts/user_tabs.js.es6
+++ b/app/assets/javascripts/user_tabs.js.es6
@@ -89,7 +89,7 @@ content on the Users#show page.
const action = $target.data('action');
const source = $target.attr('href');
this.setTab(source, action);
- return this.setCurrentAction(action);
+ return this.setCurrentAction(source, action);
}
activateTab(action) {
@@ -142,14 +142,9 @@ content on the Users#show page.
.toggle(status);
}
- setCurrentAction(action) {
- const regExp = new RegExp(`\/(${this.actions.join('|')})(\.html)?\/?$`);
- let new_state = this._location.pathname;
+ setCurrentAction(source, action) {
+ let new_state = source
new_state = new_state.replace(/\/+$/, '');
- new_state = new_state.replace(regExp, '');
- if (action !== this.defaultAction) {
- new_state += `/${action}`;
- }
new_state += this._location.search + this._location.hash;
history.replaceState({
turbolinks: true,
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 05056a73aaf..bcabda3ceb2 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -71,8 +71,8 @@
return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
});
};
- collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/u/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
- assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/u/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
+ collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
+ assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) {
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index ce489f7c3de..d11b2fe7ec2 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -194,10 +194,17 @@
pointer-events: none !important;
}
- .caret {
+ .fa-caret-down,
+ .fa-caret-up {
margin-left: 5px;
}
+ &.dropdown-toggle {
+ .fa-caret-down {
+ margin-left: 3px;
+ }
+ }
+
svg {
height: 15px;
width: 15px;
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 4a87a73a68a..baa95711329 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -1,20 +1,3 @@
-.caret {
- display: inline-block;
- width: 0;
- height: 0;
- margin-left: 2px;
- vertical-align: middle;
- border-top: $caret-width-base dashed;
- border-right: $caret-width-base solid transparent;
- border-left: $caret-width-base solid transparent;
-}
-
-.btn-group {
- .caret {
- margin-left: 0;
- }
-}
-
.dropdown {
position: relative;
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 3ac1678dd05..a55dcf4a699 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -21,7 +21,8 @@
.flash-notice, .flash-alert {
border-radius: $border-radius-default;
- .container-fluid.container-limited.flash-text {
+ .container-fluid,
+ .container-fluid.container-limited {
background: transparent;
}
}
@@ -35,12 +36,6 @@
}
}
-.content-wrapper {
- .flash-notice .container-fluid {
- background-color: transparent;
- }
-}
-
@media (max-width: $screen-md-min) {
ul.notes {
.flash-container.timeline-content {
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 37ff7e22ed1..a67d31de2f7 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -81,10 +81,10 @@ label {
.select-wrapper {
position: relative;
- .caret {
+ .fa-caret-down {
position: absolute;
right: 10px;
- top: $gl-padding;
+ top: 10px;
color: $gray-darkest;
pointer-events: none;
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index c748f856501..9823abdde1f 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -57,6 +57,10 @@ header {
&:hover, &:focus, &:active {
background-color: $background-color;
}
+
+ .fa-caret-down {
+ font-size: 15px;
+ }
}
.navbar-toggle {
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index c75dacf95d9..bcd60391543 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -21,7 +21,14 @@
padding-right: 10px;
b {
- @extend .caret;
+ display: inline-block;
+ width: 0;
+ height: 0;
+ margin-left: 2px;
+ vertical-align: middle;
+ border-top: $caret-width-base dashed;
+ border-right: $caret-width-base solid transparent;
+ border-left: $caret-width-base solid transparent;
color: $gray-darkest;
}
}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index ecc5b24e360..6e81c12aa55 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -162,6 +162,10 @@ lex
list-style: none;
overflow-y: scroll;
overflow-x: hidden;
+
+ &.is-smaller {
+ height: calc(100% - 185px);
+ }
}
.board-list-loading {
@@ -233,3 +237,31 @@ lex
margin-right: 5px;
}
}
+
+.board-new-issue-form {
+ margin: 5px;
+}
+
+.board-issue-count-holder {
+ margin-top: -3px;
+
+ .btn {
+ line-height: 12px;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+}
+
+.board-issue-count {
+ padding-right: 10px;
+ padding-left: 10px;
+ line-height: 21px;
+ border-radius: $border-radius-base;
+ border: 1px solid $border-color;
+
+ &.has-btn {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ border-width: 1px 0 1px 1px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 68fc6da6c1b..a2779704eff 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -229,9 +229,12 @@
.fa {
color: $table-text-gray;
- margin-right: 6px;
font-size: 14px;
}
+
+ svg, .fa {
+ margin-right: 0;
+ }
}
.btn-remove {
@@ -272,18 +275,8 @@
.toggle-pipeline-btn {
background-color: $gray-dark;
- .caret {
- border-top: none;
- border-bottom: 4px solid;
- }
-
&.graph-collapsed {
background-color: $white-light;
-
- .caret {
- border-bottom: none;
- border-top: 4px solid;
- }
}
}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 0fcdaf94a21..c7eac5cf4b9 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -94,7 +94,7 @@
.profile-user-bio {
// Limits the width of the user bio for readability.
max-width: 600px;
- margin: 15px auto 0;
+ margin: 10px auto;
padding: 0 16px;
}
@@ -213,29 +213,22 @@
}
.user-profile {
+
.cover-controls a {
margin-left: 5px;
}
+
.profile-header {
margin: 0 auto;
+
.avatar-holder {
width: 90px;
- display: inline-block;
- }
- .user-info {
- display: inline-block;
- text-align: left;
- vertical-align: middle;
- margin-left: 15px;
- .handle {
- color: $gl-gray-light;
- }
- .member-date {
- margin-bottom: 4px;
- }
+ margin: 0 auto 10px;
}
}
+
@media (max-width: $screen-xs-max) {
+
.cover-block {
padding-top: 20px;
}
@@ -258,10 +251,6 @@
}
}
-.user-profile-nav {
- margin-top: 15px;
-}
-
table.u2f-registrations {
th:not(:last-child), td:not(:last-child) {
border-right: solid 1px transparent;
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
index 4aa7982eab4..095af6c35eb 100644
--- a/app/controllers/projects/boards/issues_controller.rb
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -2,6 +2,7 @@ module Projects
module Boards
class IssuesController < Boards::ApplicationController
before_action :authorize_read_issue!, only: [:index]
+ before_action :authorize_create_issue!, only: [:create]
before_action :authorize_update_issue!, only: [:update]
def index
@@ -9,16 +10,23 @@ module Projects
issues = issues.page(params[:page])
render json: {
- issues: issues.as_json(
- only: [:iid, :title, :confidential],
- include: {
- assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
- labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
- }),
+ issues: serialize_as_json(issues),
size: issues.total_count
}
end
+ def create
+ list = project.board.lists.find(params[:list_id])
+ service = ::Boards::Issues::CreateService.new(project, current_user, issue_params)
+ issue = service.execute(list)
+
+ if issue.valid?
+ render json: serialize_as_json(issue)
+ else
+ render json: issue.errors, status: :unprocessable_entity
+ end
+ end
+
def update
service = ::Boards::Issues::MoveService.new(project, current_user, move_params)
@@ -43,6 +51,10 @@ module Projects
return render_403 unless can?(current_user, :read_issue, project)
end
+ def authorize_create_issue!
+ return render_403 unless can?(current_user, :admin_issue, project)
+ end
+
def authorize_update_issue!
return render_403 unless can?(current_user, :update_issue, issue)
end
@@ -54,6 +66,19 @@ module Projects
def move_params
params.permit(:id, :from_list_id, :to_list_id)
end
+
+ def issue_params
+ params.require(:issue).permit(:title).merge(request: request)
+ end
+
+ def serialize_as_json(resource)
+ resource.as_json(
+ only: [:iid, :title, :confidential],
+ include: {
+ assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
+ labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
+ })
+ end
end
end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index ef13e0677d2..96041b07647 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -159,7 +159,8 @@ class Projects::IssuesController < Projects::ApplicationController
protected
def issue
- @noteable = @issue ||= @project.issues.find_by(iid: params[:id]) || redirect_old
+ # The Sortable default scope causes performance issues when used with find_by
+ @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take || redirect_old
end
alias_method :subscribable_resource, :issue
alias_method :issuable, :issue
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 28fa4a5b141..a6626df4826 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -30,9 +30,15 @@ class Projects::LabelsController < Projects::ApplicationController
@label = @project.labels.create(label_params)
if @label.valid?
- redirect_to namespace_project_labels_path(@project.namespace, @project)
+ respond_to do |format|
+ format.html { redirect_to namespace_project_labels_path(@project.namespace, @project) }
+ format.json { render json: @label }
+ end
else
- render 'new'
+ respond_to do |format|
+ format.html { render 'new' }
+ format.json { render json: { message: @label.errors.messages }, status: 400 }
+ end
end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 8c8c56228ad..ffd9833e3b1 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -19,6 +19,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :define_diff_comment_vars, only: [:diffs]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines]
before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
+ before_action :apply_diff_view_cookie!, only: [:new_diffs]
+ before_action :build_merge_request, only: [:new, :new_diffs]
# Allow read any merge_request
before_action :authorize_read_merge_request!
@@ -210,29 +212,26 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def new
- apply_diff_view_cookie!
-
- build_merge_request
- @noteable = @merge_request
-
- @target_branches = if @merge_request.target_project
- @merge_request.target_project.repository.branch_names
- else
- []
- end
-
- @target_project = merge_request.target_project
- @source_project = merge_request.source_project
- @commits = @merge_request.compare_commits.reverse
- @commit = @merge_request.diff_head_commit
- @base_commit = @merge_request.diff_base_commit
- @diffs = @merge_request.diffs(diff_options) if @merge_request.compare
- @diff_notes_disabled = true
- @pipeline = @merge_request.pipeline
- @statuses = @pipeline.statuses.relevant if @pipeline
+ define_new_vars
+ end
- @note_counts = Note.where(commit_id: @commits.map(&:id)).
- group(:commit_id).count
+ def new_diffs
+ respond_to do |format|
+ format.html do
+ define_new_vars
+ render "new"
+ end
+ format.json do
+ @diffs = if @merge_request.can_be_created
+ @merge_request.diffs(diff_options)
+ else
+ []
+ end
+ @diff_notes_disabled = true
+
+ render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs) }
+ end
+ end
end
def create
@@ -490,6 +489,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController
)
end
+ def define_new_vars
+ @noteable = @merge_request
+
+ @target_branches = if @merge_request.target_project
+ @merge_request.target_project.repository.branch_names
+ else
+ []
+ end
+
+ @target_project = merge_request.target_project
+ @source_project = merge_request.source_project
+ @commits = @merge_request.compare_commits.reverse
+ @commit = @merge_request.diff_head_commit
+ @base_commit = @merge_request.diff_base_commit
+
+ @pipeline = @merge_request.pipeline
+ @statuses = @pipeline.statuses.relevant if @pipeline
+ @note_counts = Note.where(commit_id: @commits.map(&:id)).
+ group(:commit_id).count
+ end
+
def invalid_mr
# Render special view for MR with removed target branch
render 'invalid'
@@ -521,7 +541,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def build_merge_request
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
- @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
+ @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute
end
def compared_diff_version
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 97df74b0cfe..2cf9892edc5 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -251,9 +251,8 @@ module Ci
Ci::ProcessPipelineService.new(project, user).execute(self)
end
- def build_updated
+ def update_status
with_lock do
- reload
case latest_builds_status
when 'pending' then enqueue
when 'running' then run
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index ee3396abe04..9fa8d17e74e 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -84,13 +84,18 @@ class CommitStatus < ActiveRecord::Base
commit_status.update_attributes finished_at: Time.now
end
- after_transition any => [:success, :failed, :canceled] do |commit_status|
- commit_status.pipeline.try(:process!)
- true
- end
-
after_transition do |commit_status, transition|
- commit_status.pipeline.try(:build_updated) unless transition.loopback?
+ commit_status.pipeline.try do |pipeline|
+ break if transition.loopback?
+
+ if commit_status.complete?
+ ProcessPipelineWorker.perform_async(pipeline.id)
+ end
+
+ UpdatePipelineWorker.perform_async(pipeline.id)
+ end
+
+ true
end
after_transition [:created, :pending, :running] => :success do |commit_status|
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 071dfe54ef9..a743bf313ae 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -31,7 +31,7 @@ class MergeRequest < ActiveRecord::Base
# Temporary fields to store compare vars
# when creating new merge request
- attr_accessor :can_be_created, :compare_commits, :compare
+ attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
state_machine :state, initial: :opened do
event :close do
@@ -196,7 +196,7 @@ class MergeRequest < ActiveRecord::Base
end
def diff_size
- merge_request_diff.size
+ diffs(diff_options).size
end
def diff_base_commit
diff --git a/app/models/repository.rb b/app/models/repository.rb
index eb574555df6..bf59b74495b 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -838,6 +838,52 @@ class Repository
end
end
+ def multi_action(user:, branch:, message:, actions:, author_email: nil, author_name: nil)
+ update_branch_with_hooks(user, branch) do |ref|
+ index = rugged.index
+ parents = []
+ branch = find_branch(ref)
+
+ if branch
+ last_commit = branch.target
+ index.read_tree(last_commit.raw_commit.tree)
+ parents = [last_commit.sha]
+ end
+
+ actions.each do |action|
+ case action[:action]
+ when :create, :update, :move
+ mode =
+ case action[:action]
+ when :update
+ index.get(action[:file_path])[:mode]
+ when :move
+ index.get(action[:previous_path])[:mode]
+ end
+ mode ||= 0o100644
+
+ index.remove(action[:previous_path]) if action[:action] == :move
+
+ content = action[:encoding] == 'base64' ? Base64.decode64(action[:content]) : action[:content]
+ oid = rugged.write(content, :blob)
+
+ index.add(path: action[:file_path], oid: oid, mode: mode)
+ when :delete
+ index.remove(action[:file_path])
+ end
+ end
+
+ options = {
+ tree: index.write_tree(rugged),
+ message: message,
+ parents: parents
+ }
+ options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
+
+ Rugged::Commit.create(rugged, options)
+ end
+ end
+
def get_committer_and_author(user, email: nil, name: nil)
committer = user_to_committer(user)
author = Gitlab::Git::committer_hash(email: email, name: name) || committer
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index 0c208150fb8..1a2bad77a02 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -56,9 +56,8 @@ class BaseService
result
end
- def success
- {
- status: :success
- }
+ def success(pass_back = {})
+ pass_back[:status] = :success
+ pass_back
end
end
diff --git a/app/services/boards/issues/create_service.rb b/app/services/boards/issues/create_service.rb
new file mode 100644
index 00000000000..3701afd441f
--- /dev/null
+++ b/app/services/boards/issues/create_service.rb
@@ -0,0 +1,16 @@
+module Boards
+ module Issues
+ class CreateService < Boards::BaseService
+ def execute(list)
+ params.merge!(label_ids: [list.label_id])
+ create_issue
+ end
+
+ private
+
+ def create_issue
+ ::Issues::CreateService.new(project, current_user, params).execute
+ end
+ end
+ end
+end
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 34efd09ed9f..435a8c6e681 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -36,12 +36,7 @@ module Boards
end
def set_state
- params[:state] =
- case list.list_type.to_sym
- when :backlog then 'opened'
- when :done then 'closed'
- else 'all'
- end
+ params[:state] = list.done? ? 'closed' : 'opened'
end
def board_label_ids
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index e8465729d06..9bd4bd464f7 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -27,8 +27,9 @@ module Files
create_target_branch
end
- if commit
- success
+ result = commit
+ if result
+ success(result: result)
else
error('Something went wrong. Your changes were not committed')
end
@@ -42,6 +43,12 @@ module Files
@source_branch != @target_branch || @source_project != @project
end
+ def file_has_changed?
+ return false unless @last_commit_sha && last_commit
+
+ @last_commit_sha != last_commit.sha
+ end
+
def raise_error(message)
raise ValidationError.new(message)
end
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
new file mode 100644
index 00000000000..d28912e1301
--- /dev/null
+++ b/app/services/files/multi_service.rb
@@ -0,0 +1,124 @@
+require_relative "base_service"
+
+module Files
+ class MultiService < Files::BaseService
+ class FileChangedError < StandardError; end
+
+ def commit
+ repository.multi_action(
+ user: current_user,
+ branch: @target_branch,
+ message: @commit_message,
+ actions: params[:actions],
+ author_email: @author_email,
+ author_name: @author_name
+ )
+ end
+
+ private
+
+ def validate
+ super
+
+ params[:actions].each_with_index do |action, index|
+ unless action[:file_path].present?
+ raise_error("You must specify a file_path.")
+ end
+
+ regex_check(action[:file_path])
+ regex_check(action[:previous_path]) if action[:previous_path]
+
+ if project.empty_repo? && action[:action] != :create
+ raise_error("No files to #{action[:action]}.")
+ end
+
+ validate_file_exists(action)
+
+ case action[:action]
+ when :create
+ validate_create(action)
+ when :update
+ validate_update(action)
+ when :delete
+ validate_delete(action)
+ when :move
+ validate_move(action, index)
+ else
+ raise_error("Unknown action type `#{action[:action]}`.")
+ end
+ end
+ end
+
+ def validate_file_exists(action)
+ return if action[:action] == :create
+
+ file_path = action[:file_path]
+ file_path = action[:previous_path] if action[:action] == :move
+
+ blob = repository.blob_at_branch(params[:branch_name], file_path)
+
+ unless blob
+ raise_error("File to be #{action[:action]}d `#{file_path}` does not exist.")
+ end
+ end
+
+ def last_commit
+ Gitlab::Git::Commit.last_for_path(repository, @source_branch, @file_path)
+ end
+
+ def regex_check(file)
+ if file =~ Gitlab::Regex.directory_traversal_regex
+ raise_error(
+ 'Your changes could not be committed, because the file name, `' +
+ file +
+ '` ' +
+ Gitlab::Regex.directory_traversal_regex_message
+ )
+ end
+
+ unless file =~ Gitlab::Regex.file_path_regex
+ raise_error(
+ 'Your changes could not be committed, because the file name, `' +
+ file +
+ '` ' +
+ Gitlab::Regex.file_path_regex_message
+ )
+ end
+ end
+
+ def validate_create(action)
+ return if project.empty_repo?
+
+ if repository.blob_at_branch(params[:branch_name], action[:file_path])
+ raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.")
+ end
+ end
+
+ def validate_delete(action)
+ end
+
+ def validate_move(action, index)
+ if action[:previous_path].nil?
+ raise_error("You must supply the original file path when moving file `#{action[:file_path]}`.")
+ end
+
+ blob = repository.blob_at_branch(params[:branch_name], action[:file_path])
+
+ if blob
+ raise_error("Move destination `#{action[:file_path]}` already exists.")
+ end
+
+ if action[:content].nil?
+ blob = repository.blob_at_branch(params[:branch_name], action[:previous_path])
+ blob.load_all_data!(repository) if blob.truncated?
+ params[:actions][index][:content] = blob.data
+ end
+ end
+
+ def validate_update(action)
+ if file_has_changed?
+ raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.")
+ end
+ end
+ end
+end
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index 9e9b5b63f26..c17fdb8d1f1 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -23,12 +23,6 @@ module Files
end
end
- def file_has_changed?
- return false unless @last_commit_sha && last_commit
-
- @last_commit_sha != last_commit.sha
- end
-
def last_commit
@last_commit ||= Gitlab::Git::Commit.
last_for_path(@source_project.repository, @source_branch, @file_path)
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 76266139d09..15d7918e7fd 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -17,6 +17,11 @@ module Projects
return @project
end
+ unless allowed_fork?(forked_from_project_id)
+ @project.errors.add(:forked_from_project_id, 'is forbidden')
+ return @project
+ end
+
# Set project name from path
if @project.name.present? && @project.path.present?
# if both name and path set - everything is ok
@@ -73,6 +78,13 @@ module Projects
@project.errors.add(:namespace, "is not valid")
end
+ def allowed_fork?(source_project_id)
+ return true if source_project_id.nil?
+
+ source_project = Project.find_by(id: source_project_id)
+ current_user.can?(:fork_project, source_project)
+ end
+
def allowed_namespace?(user, namespace_id)
namespace = Namespace.find_by(id: namespace_id)
current_user.can?(:create_projects, namespace)
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index a2de4dccece..a2b23ea6171 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -16,6 +16,8 @@ module Projects
end
new_project = CreateService.new(current_user, new_params).execute
+ return new_project unless new_project.persisted?
+
builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml
index d5c21c6dffe..61c7cce20b2 100644
--- a/app/views/ci/lints/_create.html.haml
+++ b/app/views/ci/lints/_create.html.haml
@@ -16,8 +16,7 @@
%tr
%td #{stage.capitalize} Job - #{build[:name]}
%td
- %pre
- = simple_format build[:commands]
+ %pre= build[:commands]
%br
%b Tag list:
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 9d31f31c639..2a0302638ba 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -55,7 +55,7 @@
= sort_options_hash[@sort]
- else
= sort_title_recently_created
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
%li
= link_to todos_filter_path(sort: sort_value_priority) do
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index b8248a80a27..a1b39d9e1a0 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -23,7 +23,7 @@
= sort_options_hash[@sort]
- else
= sort_title_recently_created
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to explore_groups_path(sort: sort_value_recently_created) do
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index 132bbe26fe0..4cff14b096b 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -7,7 +7,7 @@
= visibility_level_label(params[:visibility_level].to_i)
- else
Any
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_projects_path(visibility_level: nil) do
@@ -27,7 +27,7 @@
= params[:tag]
- else
Any
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_projects_path(tag: nil) do
diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml
index ca6c4326d1c..23d438b2aa1 100644
--- a/app/views/groups/milestones/new.html.haml
+++ b/app/views/groups/milestones/new.html.haml
@@ -33,8 +33,8 @@
.form-group
= f.label :projects, "Projects", class: "control-label"
.col-sm-10
- = f.collection_select :project_ids, @group.projects, :id, :name,
- { selected: @group.projects.map(&:id) }, multiple: true, class: 'select2'
+ = f.collection_select :project_ids, @group.projects.non_archived, :id, :name,
+ { selected: @group.projects.non_archived.pluck(:id) }, multiple: true, class: 'select2'
.col-md-6
.form-group
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 237280872f1..7faa8bded86 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -41,7 +41,7 @@
%li.header-user.dropdown
= link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do
= image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar"
- %span.caret
+ = icon('caret-down')
.dropdown-menu-nav.dropdown-menu-align-right
%ul
%li
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index 73066150fb3..ba1502c97b6 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -12,8 +12,17 @@
%header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
%h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" }
{{ list.title }}
- %span.pull-right{ "v-if" => "list.type !== 'blank'" }
- {{ list.issuesSize }}
+ .board-issue-count-holder.pull-right.clearfix{ "v-if" => "list.type !== 'blank'" }
+ %span.board-issue-count.pull-left{ ":class" => "{ 'has-btn': list.type !== 'done' }" }
+ {{ list.issuesSize }}
+ - if can?(current_user, :admin_issue, @project)
+ %button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button",
+ "@click" => "showNewIssueForm",
+ "v-if" => "list.type !== 'done'",
+ "aria-label" => "Add an issue",
+ "title" => "Add an issue",
+ data: { placement: "top", container: "body" } }
+ = icon("plus")
- if can?(current_user, :admin_list, @project)
%board-delete{ "inline-template" => true,
":list" => "list",
@@ -26,12 +35,38 @@
":issues" => "list.issues",
":loading" => "list.loading",
":disabled" => "disabled",
+ ":show-issue-form.sync" => "showIssueForm",
":issue-link-base" => "issueLinkBase" }
.board-list-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin")
+ - if can? current_user, :create_issue, @project
+ %board-new-issue{ "inline-template" => true,
+ ":list" => "list",
+ ":show-issue-form.sync" => "showIssueForm",
+ "v-show" => "list.type !== 'done' && showIssueForm" }
+ .card.board-new-issue-form
+ %form{ "@submit" => "submit($event)" }
+ .flash-container{ "v-if" => "error" }
+ .flash-alert
+ An error occured. Please try again.
+ %label.label-light{ ":for" => "list.id + '-title'" }
+ Title
+ %input.form-control{ type: "text",
+ "v-model" => "title",
+ "v-el:input" => true,
+ ":id" => "list.id + '-title'" }
+ .clearfix.prepend-top-10
+ %button.btn.btn-success.pull-left{ type: "submit",
+ ":disabled" => "title === ''",
+ "v-el:submit-button" => true }
+ Submit issue
+ %button.btn.btn-default.pull-right{ type: "button",
+ "@click" => "cancel" }
+ Cancel
%ul.board-list{ "v-el:list" => true,
"v-show" => "!loading",
- ":data-board" => "list.id" }
+ ":data-board" => "list.id",
+ ":class" => "{ 'is-smaller': showIssueForm }" }
= render "projects/boards/components/card"
%li.board-list-count.text-center{ "v-if" => "showCount" }
= icon("spinner spin", "v-show" => "list.loadingMore" )
diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
index e8b60b54d80..d8f16022407 100644
--- a/app/views/projects/boards/components/_card.html.haml
+++ b/app/views/projects/boards/components/_card.html.haml
@@ -7,7 +7,7 @@
":issue-link-base" => "issueLinkBase",
":disabled" => "disabled",
"track-by" => "id" }
- %li.card{ ":class" => "{ 'user-can-drag': !disabled }",
+ %li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id }",
":index" => "index" }
%h4.card-title
= icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
@@ -15,7 +15,7 @@
":title" => "issue.title" }
{{ issue.title }}
.card-footer
- %span.card-number
+ %span.card-number{ "v-if" => "issue.id" }
= precede '#' do
{{ issue.id }}
%button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
@@ -26,7 +26,7 @@
":title" => "label.description",
data: { container: 'body' } }
{{ label.title }}
- %a.has-tooltip{ ":href" => "'/u/' + issue.assignee.username",
+ %a.has-tooltip{ ":href" => "'/' + issue.assignee.username",
":title" => "'Assigned to ' + issue.assignee.name",
"v-if" => "issue.assignee",
data: { container: 'body' } }
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index e889f29c816..84f38575e84 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -15,7 +15,7 @@
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
%span.light
= projects_sort_options_hash[@sort]
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_branches_path(sort: sort_value_name) do
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index b87c7a485df..36eadbd2bf1 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -67,7 +67,7 @@
.btn-group
%a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
= custom_icon('icon_play')
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |build|
%li
@@ -78,7 +78,7 @@
.btn-group
%a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'}
= icon("download")
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- artifacts.each do |build|
%li
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 29d767e7769..9fd87f84aaa 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -14,7 +14,7 @@
.dropdown.inline
%a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } }
%span.hidden-xs Options
- %span.caret.commit-options-dropdown-caret
+ = icon('caret-down', class: ".commit-options-dropdown-caret")
%ul.dropdown-menu.dropdown-menu-align-right
%li.visible-xs-block.visible-sm-block
= link_to namespace_project_tree_path(@project.namespace, @project, @commit) do
@@ -24,6 +24,8 @@
= revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false)
%li.clearfix
= cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false)
+ %li.clearfix
+ = link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit)
%li.divider
%li.dropdown-header
Download
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index 9258f4b3c25..da5b9832ba5 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -3,7 +3,7 @@
.btn.btn-grouped.btn-white.toggle-pipeline-btn
%span.toggle-btn-text Hide
%span pipeline graph
- %span.caret
+ = icon('caret-up')
- if can?(current_user, :update_pipeline, pipeline.project)
- if pipeline.builds.latest.failed.any?(&:retryable?)
= link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post
diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml
index 16d134eb6b6..99cb4222377 100644
--- a/app/views/projects/deployments/_actions.haml
+++ b/app/views/projects/deployments/_actions.haml
@@ -6,7 +6,7 @@
.dropdown
%a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
= custom_icon('icon_play')
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |action|
%li
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 576e7ef021a..067cf595da3 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -1,4 +1,5 @@
- show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true)
+- can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project)
- diff_files = diffs.diff_files
.content-block.oneline-block.files-changed
@@ -20,7 +21,7 @@
- if diff_files.overflow?
= render 'projects/diffs/warning', diff_files: diff_files
-.files{data: {can_create_note: (!@diff_notes_disabled && can?(current_user, :create_note, diffs.project))}}
+.files{ data: { can_create_note: can_create_note } }
- diff_files.each_with_index do |diff_file, index|
- diff_commit = commit_for_diff(diff_file)
- blob = diff_file.blob(diff_commit)
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index a04d53e02bf..d19422c8657 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -100,7 +100,8 @@
= f.check_box :container_registry_enabled
%strong Container Registry
%br
- %span.descr Enable Container Registry for this repository
+ %span.descr Enable Container Registry for this project
+ = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank'
= render 'merge_request_settings', f: f
%hr
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index bacc5708e4b..abf4f697f86 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -15,7 +15,7 @@
= sort_options_hash[@sort]
- else
= sort_title_recently_created
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
- excluded_filters = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id]
diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml
index 4c5dd9b88bf..1b0dbbb8111 100644
--- a/app/views/projects/group_links/index.html.haml
+++ b/app/views/projects/group_links/index.html.haml
@@ -16,7 +16,7 @@
= label_tag :link_group_access, "Max access level", class: "label-light"
.select-wrapper
= select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
- %span.caret
+ = icon('caret-down')
.form-group
= label_tag :expires_at, 'Access expiration date', class: 'label-light'
.clearable-input
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 3fb4191c60e..cbdea209847 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -23,7 +23,7 @@
.issuable-actions
.clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
- %span.caret
+ = icon('caret-down')
Options
.dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul
diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml
index 73c6f2a046c..71f7f354d72 100644
--- a/app/views/projects/labels/_label.html.haml
+++ b/app/views/projects/labels/_label.html.haml
@@ -5,7 +5,7 @@
.visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown
%button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } }
Options
- %span.caret
+ = icon('caret-down')
.dropdown-menu.dropdown-menu-align-right
%ul
%li
diff --git a/app/views/projects/merge_requests/_new_diffs.html.haml b/app/views/projects/merge_requests/_new_diffs.html.haml
new file mode 100644
index 00000000000..74367ab9b7b
--- /dev/null
+++ b/app/views/projects/merge_requests/_new_diffs.html.haml
@@ -0,0 +1 @@
+= render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 00bd4e143df..88d8013a0d1 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -19,34 +19,32 @@
.mr-compare.merge-request
%ul.merge-request-tabs.nav-links.no-top.no-bottom
- %li.commits-tab
+ %li.commits-tab.active
= link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
Commits
%span.badge= @commits.size
- if @pipeline
- %li.builds-tab.active
+ %li.builds-tab
= link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do
Builds
%span.badge= @statuses.size
- %li.diffs-tab.active
- = link_to url_for(params), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
+ %li.diffs-tab
+ = link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do
Changes
- %span.badge= @diffs.real_size
+ %span.badge= @merge_request.diff_size
.tab-content
- #commits.commits.tab-pane
+ #commits.commits.tab-pane.active
= render "projects/merge_requests/show/commits"
- #diffs.diffs.tab-pane.active
- - if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
- .alert.alert-danger
- %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits.
- %p To preserve performance the line changes are not shown.
- - else
- = render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false
+ #diffs.diffs.tab-pane
+ - # This tab is always loaded via AJAX
- if @pipeline
#builds.builds.tab-pane
= render "projects/merge_requests/show/builds"
+ .mr-loading-status
+ = spinner
+
:javascript
$('.assign-to-me-link').on('click', function(e){
$('#merge_request_assignee_id').val("#{current_user.id}").trigger("change");
@@ -54,6 +52,6 @@
});
:javascript
var merge_request = new MergeRequest({
- action: "#{(@show_changes_tab ? 'diffs' : 'new')}",
- setUrl: false
+ action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}",
+ buildsLoaded: "#{@pipeline ? 'true' : 'false'}"
});
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 9f34ca9ff4e..46a2d862c91 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -22,7 +22,7 @@
%span.dropdown.inline.prepend-left-5
%a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} }
Download as
- %span.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch)
%li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff)
diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml
index e35291dff7d..9f2a0f5d99a 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -19,7 +19,7 @@
.issuable-actions
.clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
- %span.caret
+ = icon('caret-down')
Options
.dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul
diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml
index 904452fcc4f..988ac0feae1 100644
--- a/app/views/projects/merge_requests/show/_versions.html.haml
+++ b/app/views/projects/merge_requests/show/_versions.html.haml
@@ -9,7 +9,7 @@
latest version
- else
version #{version_index(@merge_request_diff)}
- %span.caret
+ = icon('caret-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Version:
@@ -39,7 +39,7 @@
version #{version_index(@start_version)}
- else
#{@merge_request.target_branch}
- %span.caret
+ = icon('caret-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Compared with:
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index b5f5e11d4c3..5b7f83c344f 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -49,11 +49,12 @@
.mr-widget-heading
.ci_widget.ci-success
= ci_icon_for_status("success")
- %span.hidden-sm
+ %span
Deployed to
= succeed '.' do
= link_to environment.name, environment_path(environment), class: 'environment'
- external_url = environment.external_url
- if external_url
= link_to external_url, target: '_blank' do
- = icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true)
+ %span.hidden-xs View on #{external_url.gsub(/\A.*?:\/\//, '')}
+ = icon('external-link', right: true)
diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml
index bf2e76f0083..ce43ca3a286 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -12,7 +12,7 @@
Merge When Build Succeeds
- unless @project.only_allow_merge_if_build_succeeds?
= button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do
- %span.caret
+ = icon('caret-down')
%span.sr-only
Select Merge Moment
%ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' }
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
index 9773b8438ec..32e1f8a21b0 100644
--- a/app/views/projects/snippets/_actions.html.haml
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -12,7 +12,7 @@
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
Options
- %span.caret
+ = icon('caret-down')
.dropdown-menu.dropdown-menu-full-width
%ul
- if can?(current_user, :create_project_snippet, @project)
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 6adbe9351dc..7a0d9dcc94f 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -14,7 +14,7 @@
%button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} }
%span.light
= @sort.humanize
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_tags_path(sort: nil) do
diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml
index 51622931e24..fbbf6f358c5 100644
--- a/app/views/shared/_new_project_item_select.html.haml
+++ b/app/views/shared/_new_project_item_select.html.haml
@@ -3,7 +3,7 @@
= project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at' }
%a.btn.btn-new.new-project-item-select-button
= local_assigns[:label]
- %b.caret
+ = icon('caret-down')
:javascript
$('.new-project-item-select-button').on('click', function() {
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index 36bbac6fbf5..68e05cb72e1 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -5,7 +5,7 @@
= sort_options_hash[@sort]
- else
= sort_title_recently_created
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
%li
= link_to page_filter_path(sort: sort_value_priority, label: true) do
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 6eafc309a13..31620297be0 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -38,7 +38,7 @@
%input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" }
- if can?(current_user, :admin_list, @project)
.dropdown.pull-right
- %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, project_id: @project.try(:id) } }
+ %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } }
Create new list
.dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" }
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index 92d5cb307ae..6d307611640 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -8,7 +8,7 @@
- classes = local_assigns.fetch(:classes, [])
- selected = local_assigns.fetch(:selected, nil)
- selected_toggle = local_assigns.fetch(:selected_toggle, nil)
-- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", project_id: project.try(:id), labels: labels_filter_path, default_label: "Labels"}
+- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Labels"}
- dropdown_data.merge!(data_options)
- classes << 'js-extra-options' if extra_options
- classes << 'js-filter-submit' if filter_submit
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 4e3bb8a8285..f8059988038 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -129,7 +129,7 @@
- selected_labels.each do |label|
= hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
.dropdown
- %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}}
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}}
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?)}
= multi_label_name(selected_labels, "Labels")
= icon('chevron-down')
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index ff1cf966a9b..feaa5570c21 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -11,7 +11,7 @@
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
- %span.caret
+ = icon('caret-down')
.sr-only Toggle dropdown
- else
%button.dropdown-new.btn.btn-default.notifications-btn#notifications-button{ type: "button", data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
index c446dc3bdc1..1d0e549ed3d 100644
--- a/app/views/snippets/_actions.html.haml
+++ b/app/views/snippets/_actions.html.haml
@@ -12,7 +12,7 @@
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
Options
- %span.caret
+ = icon('caret-down')
.dropdown-menu.dropdown-menu-full-width
%ul
%li
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 60fc0c0daf6..1e0752bd3c3 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -40,11 +40,11 @@
.user-info
.cover-title
= @user.name
- %span.handle
- @#{@user.username}
.cover-desc.member-date
%span.middle-dot-divider
+ @#{@user.username}
+ %span.middle-dot-divider
Member since #{@user.created_at.to_s(:medium)}
.cover-desc
@@ -82,7 +82,7 @@
%ul.nav-links.center.user-profile-nav
%li.js-activity-tab
- = link_to user_calendar_activities_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do
+ = link_to user_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do
Activity
%li.js-groups-tab
= link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do
diff --git a/app/workers/process_pipeline_worker.rb b/app/workers/process_pipeline_worker.rb
new file mode 100644
index 00000000000..26ea5f1c24d
--- /dev/null
+++ b/app/workers/process_pipeline_worker.rb
@@ -0,0 +1,10 @@
+class ProcessPipelineWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :default
+
+ def perform(pipeline_id)
+ Ci::Pipeline.find_by(id: pipeline_id)
+ .try(:process!)
+ end
+end
diff --git a/app/workers/update_pipeline_worker.rb b/app/workers/update_pipeline_worker.rb
new file mode 100644
index 00000000000..6ef5678073e
--- /dev/null
+++ b/app/workers/update_pipeline_worker.rb
@@ -0,0 +1,10 @@
+class UpdatePipelineWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :default
+
+ def perform(pipeline_id)
+ Ci::Pipeline.find_by(id: pipeline_id)
+ .try(:update_status)
+ end
+end
diff --git a/config/application.rb b/config/application.rb
index 5dbe5a8120b..962ffe0708d 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -50,6 +50,7 @@ module Gitlab
# - Build variables (:variables)
# - GitLab Pages SSL cert/key info (:certificate, :encrypted_key)
# - Webhook URLs (:hook)
+ # - GitLab-shell secret token (:secret_token)
# - Sentry DSN (:sentry_dsn)
# - Deploy keys (:key)
config.filter_parameters += %i(
@@ -62,6 +63,7 @@ module Gitlab
password
password_confirmation
private_token
+ secret_token
sentry_dsn
variables
)
diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb
index 5892c1de024..4f30d1265c8 100644
--- a/config/initializers/sentry.rb
+++ b/config/initializers/sentry.rb
@@ -18,6 +18,8 @@ if Rails.env.production?
# Sanitize fields based on those sanitized from Rails.
config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s)
+ # Sanitize authentication headers
+ config.sanitize_http_headers = %w[Authorization Private-Token]
config.tags = { program: Gitlab::Sentry.program_context }
end
end
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 5b3e25d5e3d..47a8a0a53d4 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -1,3 +1,11 @@
+require 'constraints/group_url_constrainer'
+
+constraints(GroupUrlConstrainer.new) do
+ scope(path: ':id', as: :group, controller: :groups) do
+ get '/', action: :show
+ end
+end
+
resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do
member do
get :issues
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 224ec7e8324..e8807ef06a7 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -285,6 +285,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
get :update_branches
get :diff_for_path
post :bulk_update
+ get :new_diffs, path: 'new/diffs'
end
resources :discussions, only: [], constraints: { id: /\h{40}/ } do
@@ -424,7 +425,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only:
post :generate
end
- resources :issues, only: [:index]
+ resources :issues, only: [:index, :create]
end
end
end
diff --git a/config/routes/user.rb b/config/routes/user.rb
index bbb30cedd4d..c287039ba26 100644
--- a/config/routes/user.rb
+++ b/config/routes/user.rb
@@ -1,15 +1,7 @@
-scope(path: 'u/:username',
- as: :user,
- constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ },
- controller: :users) do
- get :calendar
- get :calendar_activities
- get :groups
- get :projects
- get :contributed, as: :contributed_projects
- get :snippets
- get '/', action: :show
-end
+require 'constraints/user_url_constrainer'
+
+get '/u/:username', to: redirect('/%{username}'),
+ constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks,
registrations: :registrations,
@@ -21,3 +13,22 @@ devise_scope :user do
get '/users/auth/:provider/omniauth_error' => 'omniauth_callbacks#omniauth_error', as: :omniauth_error
get '/users/almost_there' => 'confirmations#almost_there'
end
+
+constraints(UserUrlConstrainer.new) do
+ scope(path: ':username', as: :user, controller: :users) do
+ get '/', action: :show
+ end
+end
+
+scope(path: 'u/:username',
+ as: :user,
+ constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ },
+ controller: :users) do
+ get :calendar
+ get :calendar_activities
+ get :groups
+ get :projects
+ get :contributed, as: :contributed_projects
+ get :snippets
+ get '/', to: redirect('/%{username}')
+end
diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb
index 650b410595c..803cbca584d 100644
--- a/db/fixtures/development/14_pipelines.rb
+++ b/db/fixtures/development/14_pipelines.rb
@@ -34,7 +34,7 @@ class Gitlab::Seeder::Pipelines
rescue ActiveRecord::RecordInvalid
print 'F'
ensure
- pipeline.build_updated
+ pipeline.update_status
end
end
end
diff --git a/doc/README.md b/doc/README.md
index 4ff1a0582c8..9017b143260 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -6,7 +6,7 @@
- [API](api/README.md) Automate GitLab via a simple and powerful API.
- [CI/CD](ci/README.md) GitLab Continuous Integration (CI) and Continuous Delivery (CD) getting started, `.gitlab-ci.yml` options, and examples.
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
-- [Container Registry](container_registry/README.md) Learn how to use GitLab Container Registry.
+- [Container Registry](user/project/container_registry.md) Learn how to use GitLab Container Registry.
- [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
- [Importing to GitLab](workflow/importing/README.md).
- [Importing and exporting projects between instances](user/project/settings/import_export.md).
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index c5611e2a121..d7cfb464f74 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -1,42 +1,32 @@
-# GitLab Container Registry Administration
+# GitLab Container Registry administration
> [Introduced][ce-4040] in GitLab 8.8.
-With the Docker Container Registry integrated into GitLab, every project can
-have its own space to store its Docker images.
-
-You can read more about Docker Registry at https://docs.docker.com/registry/introduction/.
-
---
-<!-- START doctoc generated TOC please keep comment here to allow auto update -->
-<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
-**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
+> **Notes:**
+- Container Registry manifest `v1` support was added in GitLab 8.9 to support
+ Docker versions earlier than 1.10.
+- This document is about the admin guide. To learn how to use GitLab Container
+ Registry [user documentation](../user/project/container_registry.md).
-- [Enable the Container Registry](#enable-the-container-registry)
-- [Container Registry domain configuration](#container-registry-domain-configuration)
- - [Configure Container Registry under an existing GitLab domain](#configure-container-registry-under-an-existing-gitlab-domain)
- - [Configure Container Registry under its own domain](#configure-container-registry-under-its-own-domain)
-- [Disable Container Registry site-wide](#disable-container-registry-site-wide)
-- [Disable Container Registry per project](#disable-container-registry-per-project)
-- [Disable Container Registry for new projects site-wide](#disable-container-registry-for-new-projects-site-wide)
-- [Container Registry storage path](#container-registry-storage-path)
-- [Container Registry storage driver](#container-registry-storage-driver)
-- [Storage limitations](#storage-limitations)
-- [Changelog](#changelog)
+With the Container Registry integrated into GitLab, every project can have its
+own space to store its Docker images.
-<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+You can read more about the Container Registry at
+https://docs.docker.com/registry/introduction/.
## Enable the Container Registry
**Omnibus GitLab installations**
All you have to do is configure the domain name under which the Container
-Registry will listen to. Read [#container-registry-domain-configuration](#container-registry-domain-configuration)
+Registry will listen to. Read
+[#container-registry-domain-configuration](#container-registry-domain-configuration)
and pick one of the two options that fits your case.
>**Note:**
-The container Registry works under HTTPS by default. Using HTTP is possible
+The container registry works under HTTPS by default. Using HTTP is possible
but not recommended and out of the scope of this document.
Read the [insecure Registry documentation][docker-insecure] if you want to
implement this.
@@ -47,7 +37,7 @@ implement this.
If you have installed GitLab from source:
-1. You will have to [install Docker Registry][registry-deploy] by yourself.
+1. You will have to [install Registry][registry-deploy] by yourself.
1. After the installation is complete, you will have to configure the Registry's
settings in `gitlab.yml` in order to enable it.
1. Use the sample NGINX configuration file that is found under
@@ -80,11 +70,13 @@ where:
| `issuer` | This should be the same value as configured in Registry's `issuer`. Read the [token auth configuration documentation][token-config]. |
>**Note:**
-GitLab does not ship with a Registry init file. Hence, [restarting GitLab][restart gitlab]
-will not restart the Registry should you modify its settings. Read the upstream
-documentation on how to achieve that.
+A Registry init file is not shipped with GitLab if you install it from source.
+Hence, [restarting GitLab][restart gitlab] will not restart the Registry should
+you modify its settings. Read the upstream documentation on how to achieve that.
-The Docker Registry configuration will need `container_registry` as the service and `https://gitlab.example.com/jwt/auth` as the realm:
+At the absolute minimum, make sure your [Registry configuration][registry-auth]
+has `container_registry` as the service and `https://gitlab.example.com/jwt/auth`
+as the realm:
```
auth:
@@ -275,12 +267,6 @@ Registry application itself.
1. Save the file and [restart GitLab][] for the changes to take effect.
-## Disable Container Registry per project
-
-If Registry is enabled in your GitLab instance, but you don't need it for your
-project, you can disable it from your project's settings. Read the user guide
-on how to achieve that.
-
## Disable Container Registry for new projects site-wide
If the Container Registry is enabled, then it will be available on all new
@@ -436,6 +422,46 @@ storage:
enabled: true
```
+## Change the registry's internal port
+
+> **Note:**
+This is not to be confused with the port that GitLab itself uses to expose
+the Registry to the world.
+
+The Registry server listens on localhost at port `5000` by default,
+which is the address for which the Registry server should accept connections.
+In the examples below we set the Registry's port to `5001`.
+
+**Omnibus GitLab**
+
+1. Open `/etc/gitlab/gitlab.rb` and set `registry['registry_http_addr']`:
+
+ ```ruby
+ registry['registry_http_addr'] = "localhost:5001"
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**Installations from source**
+
+1. Open the configuration file of your Registry server and edit the
+ [`http:addr`][registry-http-config] value:
+
+ ```
+ http
+ addr: localhost:5001
+ ```
+
+1. Save the file and restart the Registry server.
+
+## Disable Container Registry per project
+
+If Registry is enabled in your GitLab instance, but you don't need it for your
+project, you can disable it from your project's settings. Read the user guide
+on how to achieve that.
+
## Storage limitations
Currently, there is no storage limitation, which means a user can upload an
@@ -455,6 +481,8 @@ configurable in future releases.
[docker-insecure]: https://docs.docker.com/registry/insecure/
[registry-deploy]: https://docs.docker.com/registry/deploying/
[storage-config]: https://docs.docker.com/registry/configuration/#storage
+[registry-http-config]: https://docs.docker.com/registry/configuration/#http
+[registry-auth]: https://docs.docker.com/registry/configuration/#auth
[token-config]: https://docs.docker.com/registry/configuration/#token
[8-8-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-8-stable/doc/administration/container_registry.md
[registry-ssl]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/nginx/registry-ssl
diff --git a/doc/api/boards.md b/doc/api/boards.md
new file mode 100644
index 00000000000..28681719f43
--- /dev/null
+++ b/doc/api/boards.md
@@ -0,0 +1,251 @@
+# Boards
+
+Every API call to boards must be authenticated.
+
+If a user is not a member of a project and the project is private, a `GET`
+request on that project will result to a `404` status code.
+
+## Project Board
+
+Lists Issue Boards in the given project.
+
+```
+GET /projects/:id/boards
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/boards
+```
+
+Example response:
+
+```json
+[
+ {
+ "id" : 1,
+ "lists" : [
+ {
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+ },
+ {
+ "id" : 2,
+ "label" : {
+ "name" : "Ready",
+ "color" : "#FF0000",
+ "description" : null
+ },
+ "position" : 2
+ },
+ {
+ "id" : 3,
+ "label" : {
+ "name" : "Production",
+ "color" : "#FF5F00",
+ "description" : null
+ },
+ "position" : 3
+ }
+ ]
+ }
+]
+```
+
+## List board lists
+
+Get a list of the board's lists.
+Does not include `backlog` and `done` lists
+
+```
+GET /projects/:id/boards/:board_id/lists
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `board_id` | integer | yes | The ID of a board |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists
+```
+
+Example response:
+
+```json
+[
+ {
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+ },
+ {
+ "id" : 2,
+ "label" : {
+ "name" : "Ready",
+ "color" : "#FF0000",
+ "description" : null
+ },
+ "position" : 2
+ },
+ {
+ "id" : 3,
+ "label" : {
+ "name" : "Production",
+ "color" : "#FF5F00",
+ "description" : null
+ },
+ "position" : 3
+ }
+]
+```
+
+## Single board list
+
+Get a single board list.
+
+```
+GET /projects/:id/boards/:board_id/lists/:list_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `board_id` | integer | yes | The ID of a board |
+| `list_id`| integer | yes | The ID of a board's list |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1
+```
+
+Example response:
+
+```json
+{
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+}
+```
+
+## New board list
+
+Creates a new Issue Board list.
+
+If the operation is successful, a status code of `200` and the newly-created
+list is returned. If an error occurs, an error number and a message explaining
+the reason is returned.
+
+```
+POST /projects/:id/boards/:board_id/lists
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `board_id` | integer | yes | The ID of a board |
+| `label_id` | integer | yes | The ID of a label |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists?label_id=5
+```
+
+Example response:
+
+```json
+{
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+}
+```
+
+## Edit board list
+
+Updates an existing Issue Board list. This call is used to change list position.
+
+If the operation is successful, a code of `200` and the updated board list is
+returned. If an error occurs, an error number and a message explaining the
+reason is returned.
+
+```
+PUT /projects/:id/boards/:board_id/lists/:list_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `board_id` | integer | yes | The ID of a board |
+| `list_id` | integer | yes | The ID of a board's list |
+| `position` | integer | yes | The position of the list |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1?position=2
+```
+
+Example response:
+
+```json
+{
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+}
+```
+
+## Delete a board list
+
+Only for admins and project owners. Soft deletes the board list in question.
+If the operation is successful, a status code `200` is returned. In case you cannot
+destroy this board list, or it is not present, code `404` is given.
+
+```
+DELETE /projects/:id/boards/:board_id/lists/:list_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `board_id` | integer | yes | The ID of a board |
+| `list_id` | integer | yes | The ID of a board's list |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1
+```
+Example response:
+
+```json
+{
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+}
+```
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 682151d4b1d..3e20beefb8a 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -46,6 +46,91 @@ Example response:
]
```
+## Create a commit with multiple files and actions
+
+> [Introduced][ce-6096] in GitLab 8.13.
+
+Create a commit by posting a JSON payload
+
+```
+POST /projects/:id/repository/commits
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME |
+| `branch_name` | string | yes | The name of a branch |
+| `commit_message` | string | yes | Commit message |
+| `actions[]` | array | yes | An array of action hashes to commit as a batch. See the next table for what attributes it can take. |
+| `author_email` | string | no | Specify the commit author's email address |
+| `author_name` | string | no | Specify the commit author's name |
+
+
+| `actions[]` Attribute | Type | Required | Description |
+| --------------------- | ---- | -------- | ----------- |
+| `action` | string | yes | The action to perform, `create`, `delete`, `move`, `update` |
+| `file_path` | string | yes | Full path to the file. Ex. `lib/class.rb` |
+| `previous_path` | string | no | Original full path to the file being moved. Ex. `lib/class1.rb` |
+| `content` | string | no | File content, required for all except `delete`. Optional for `move` |
+| `encoding` | string | no | `text` or `base64`. `text` is default. |
+
+```bash
+PAYLOAD=$(cat << 'JSON'
+{
+ "branch_name": "master",
+ "commit_message": "some commit message",
+ "actions": [
+ {
+ "action": "create",
+ "file_path": "foo/bar",
+ "content": "some content"
+ },
+ {
+ "action": "delete",
+ "file_path": "foo/bar2",
+ },
+ {
+ "action": "move",
+ "file_path": "foo/bar3",
+ "previous_path": "foo/bar4",
+ "content": "some content"
+ },
+ {
+ "action": "update",
+ "file_path": "foo/bar5",
+ "content": "new content"
+ }
+ ]
+}
+JSON
+)
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data "$PAYLOAD" https://gitlab.example.com/api/v3/projects/1/repository/commits
+```
+
+Example response:
+```json
+{
+ "id": "ed899a2f4b50b4370feeea94676502b42383c746",
+ "short_id": "ed899a2f4b5",
+ "title": "some commit message",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dzaporozhets@sphereconsultinginc.com",
+ "created_at": "2016-09-20T09:26:24.000-07:00",
+ "message": "some commit message",
+ "parent_ids": [
+ "ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba"
+ ],
+ "committed_date": "2016-09-20T09:26:24.000-07:00",
+ "authored_date": "2016-09-20T09:26:24.000-07:00",
+ "stats": {
+ "additions": 2,
+ "deletions": 2,
+ "total": 4
+ },
+ "status": null
+}
+```
+
## Get a single commit
Get a specific commit identified by the commit hash or name of a branch or tag.
@@ -343,3 +428,5 @@ Example response:
"finished_at" : "2016-01-19T09:05:50.365Z"
}
```
+
+[ce-6096]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6096 "Multi-file commit"
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 16868554c1f..cdf5ecc7a84 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -858,27 +858,45 @@ job:
## Git Strategy
-> Introduced in GitLab 8.9 as an experimental feature. May change in future
- releases or be removed completely.
+> Introduced in GitLab 8.9 as an experimental feature. May change or be removed
+ completely in future releases. `GIT_STRATEGY=none` requires GitLab Runner
+ v1.7+.
+
+You can set the `GIT_STRATEGY` used for getting recent application code, either
+in the global [`variables`](#variables) section or the [`variables`](#job-variables)
+section for individual jobs. If left unspecified, the default from project
+settings will be used.
-You can set the `GIT_STRATEGY` used for getting recent application code. `clone`
-is slower, but makes sure you have a clean directory before every build. `fetch`
-is faster. `GIT_STRATEGY` can be specified in the global `variables` section or
-in the `variables` section for individual jobs. If it's not specified, then the
-default from project settings will be used.
+There are three possible values: `clone`, `fetch`, and `none`.
+
+`clone` is the slowest option. It clones the repository from scratch for every
+job, ensuring that the project workspace is always pristine.
```
variables:
GIT_STRATEGY: clone
```
-or
+`fetch` is faster as it re-uses the project workspace (falling back to `clone`
+if it doesn't exist). `git clean` is used to undo any changes made by the last
+job, and `git fetch` is used to retrieve commits made since the last job ran.
```
variables:
GIT_STRATEGY: fetch
```
+`none` also re-uses the project workspace, but skips all Git operations
+(including GitLab Runner's pre-clone script, if present). It is mostly useful
+for jobs that operate exclusively on artifacts (e.g., `deploy`). Git repository
+data may be present, but it is certain to be out of date, so you should only
+rely on files brought into the project workspace from cache or artifacts.
+
+```
+variables:
+ GIT_STRATEGY: none
+```
+
## Shallow cloning
> Introduced in GitLab 8.9 as an experimental feature. May change in future
diff --git a/doc/container_registry/README.md b/doc/container_registry/README.md
index d7740647a91..fe3e4681ba7 100644
--- a/doc/container_registry/README.md
+++ b/doc/container_registry/README.md
@@ -1,98 +1 @@
-# GitLab Container Registry
-
-> [Introduced][ce-4040] in GitLab 8.8. Docker Registry manifest
-`v1` support was added in GitLab 8.9 to support Docker versions earlier than 1.10.
-
-> **Note:**
-This document is about the user guide. To learn how to enable GitLab Container
-Registry across your GitLab instance, visit the
-[administrator documentation](../administration/container_registry.md).
-
-With the Docker Container Registry integrated into GitLab, every project can
-have its own space to store its Docker images.
-
-You can read more about Docker Registry at https://docs.docker.com/registry/introduction/.
-
----
-
-## Enable the Container Registry for your project
-
-1. First, ask your system administrator to enable GitLab Container Registry
- following the [administration documentation](../administration/container_registry.md).
- If you are using GitLab.com, this is enabled by default so you can start using
- the Registry immediately.
-
-1. Go to your project's settings and enable the **Container Registry** feature
- on your project. For new projects this might be enabled by default. For
- existing projects you will have to explicitly enable it.
-
- ![Enable Container Registry](img/project_feature.png)
-
-## Build and push images
-
-After you save your project's settings, you should see a new link in the
-sidebar called **Container Registry**. Following this link will get you to
-your project's Registry panel where you can see how to login to the Container
-Registry using your GitLab credentials.
-
-For example if the Registry's URL is `registry.example.com`, the you should be
-able to login with:
-
-```
-docker login registry.example.com
-```
-
-Building and publishing images should be a straightforward process. Just make
-sure that you are using the Registry URL with the namespace and project name
-that is hosted on GitLab:
-
-```
-docker build -t registry.example.com/group/project .
-docker push registry.example.com/group/project
-```
-
-## Use images from GitLab Container Registry
-
-To download and run a container from images hosted in GitLab Container Registry,
-use `docker run`:
-
-```
-docker run [options] registry.example.com/group/project [arguments]
-```
-
-For more information on running Docker containers, visit the
-[Docker documentation][docker-docs].
-
-## Control Container Registry from within GitLab
-
-GitLab offers a simple Container Registry management panel. Go to your project
-and click **Container Registry** in the left sidebar.
-
-This view will show you all tags in your project and will easily allow you to
-delete them.
-
-![Container Registry panel](img/container_registry.png)
-
-## Build and push images using GitLab CI
-
-> **Note:**
-This feature requires GitLab 8.8 and GitLab Runner 1.2.
-
-Make sure that your GitLab Runner is configured to allow building Docker images by
-following the [Using Docker Build](../ci/docker/using_docker_build.md)
-and [Using the GitLab Container Registry documentation](../ci/docker/using_docker_build.md#using-the-gitlab-container-registry).
-
-## Limitations
-
-In order to use a container image from your private project as an `image:` in
-your `.gitlab-ci.yml`, you have to follow the
-[Using a private Docker Registry][private-docker]
-documentation. This workflow will be simplified in the future.
-
-## Troubleshooting
-
-See [the GitLab Docker registry troubleshooting guide](troubleshooting.md).
-
-[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040
-[docker-docs]: https://docs.docker.com/engine/userguide/intro/
-[private-docker]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#using-a-private-docker-registry
+This document was moved in [user/project/container_registry](../user/project/container_registry.md).
diff --git a/doc/container_registry/img/container_registry.png b/doc/container_registry/img/container_registry.png
deleted file mode 100644
index 57d6f9f22c5..00000000000
--- a/doc/container_registry/img/container_registry.png
+++ /dev/null
Binary files differ
diff --git a/doc/container_registry/img/project_feature.png b/doc/container_registry/img/project_feature.png
deleted file mode 100644
index a59b4f82b56..00000000000
--- a/doc/container_registry/img/project_feature.png
+++ /dev/null
Binary files differ
diff --git a/doc/container_registry/troubleshooting.md b/doc/container_registry/troubleshooting.md
index 14c4a7d9a63..2f8cd37b488 100644
--- a/doc/container_registry/troubleshooting.md
+++ b/doc/container_registry/troubleshooting.md
@@ -1,141 +1 @@
-# Troubleshooting the GitLab Container Registry
-
-## Basic Troubleshooting
-
-1. Check to make sure that the system clock on your Docker client and GitLab server have
- been synchronized (e.g. via NTP).
-
-2. If you are using an S3-backed Registry, double check that the IAM
- permissions and the S3 credentials (including region) are correct. See [the
- sample IAM policy](https://docs.docker.com/registry/storage-drivers/s3/)
- for more details.
-
-3. Check the Registry logs (e.g. `/var/log/gitlab/registry/current`) and the GitLab production logs
- for errors (e.g. `/var/log/gitlab/gitlab-rails/production.log`). You may be able to find clues
- there.
-
-## Advanced Troubleshooting
-
->**NOTE:** The following section is only recommended for experts.
-
-Sometimes it's not obvious what is wrong, and you may need to dive deeper into
-the communication between the Docker client and the Registry to find out
-what's wrong. We will use a concrete example in the past to illustrate how to
-diagnose a problem with the S3 setup.
-
-### Unexpected 403 error during push
-
-A user attempted to enable an S3-backed Registry. The `docker login` step went
-fine. However, when pushing an image, the output showed:
-
-```
-The push refers to a repository [s3-testing.myregistry.com:4567/root/docker-test]
-dc5e59c14160: Pushing [==================================================>] 14.85 kB
-03c20c1a019a: Pushing [==================================================>] 2.048 kB
-a08f14ef632e: Pushing [==================================================>] 2.048 kB
-228950524c88: Pushing 2.048 kB
-6a8ecde4cc03: Pushing [==> ] 9.901 MB/205.7 MB
-5f70bf18a086: Pushing 1.024 kB
-737f40e80b7f: Waiting
-82b57dbc5385: Waiting
-19429b698a22: Waiting
-9436069b92a3: Waiting
-error parsing HTTP 403 response body: unexpected end of JSON input: ""
-```
-
-This error is ambiguous, as it's not clear whether the 403 is coming from the
-GitLab Rails application, the Docker Registry, or something else. In this
-case, since we know that since the login succeeded, we probably need to look
-at the communication between the client and the Registry.
-
-The REST API between the Docker client and Registry is [described
-here](https://docs.docker.com/registry/spec/api/). Normally, one would just
-use Wireshark or tcpdump to capture the traffic and see where things went
-wrong. However, since all communication between Docker clients and servers
-are done over HTTPS, it's a bit difficult to decrypt the traffic quickly even
-if you know the private key. What can we do instead?
-
-One way would be to disable HTTPS by setting up an [insecure
-Registry](https://docs.docker.com/registry/insecure/). This could introduce a
-security hole and is only recommended for local testing. If you have a
-production system and can't or don't want to do this, there is another way:
-use mitmproxy, which stands for Man-in-the-Middle Proxy.
-
-### mitmproxy
-
-[mitmproxy](https://mitmproxy.org/) allows you to place a proxy between your
-client and server to inspect all traffic. One wrinkle is that your system
-needs to trust the mitmproxy SSL certificates for this to work.
-
-The following installation instructions assume you are running Ubuntu:
-
-1. Install mitmproxy (see http://docs.mitmproxy.org/en/stable/install.html)
-1. Run `mitmproxy --port 9000` to generate its certificates.
- Enter <kbd>CTRL</kbd>-<kbd>C</kbd> to quit.
-1. Install the certificate from `~/.mitmproxy` to your system:
-
- ```sh
- sudo cp ~/.mitmproxy/mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy-ca-cert.crt
- sudo update-ca-certificates
- ```
-
-If successful, the output should indicate that a certificate was added:
-
-```sh
-Updating certificates in /etc/ssl/certs... 1 added, 0 removed; done.
-Running hooks in /etc/ca-certificates/update.d....done.
-```
-
-To verify that the certificates are properly installed, run:
-
-```sh
-mitmproxy --port 9000
-```
-
-This will run mitmproxy on port `9000`. In another window, run:
-
-```sh
-curl --proxy http://localhost:9000 https://httpbin.org/status/200
-```
-
-If everything is setup correctly, you will see information on the mitmproxy window and
-no errors from the curl commands.
-
-### Running the Docker daemon with a proxy
-
-For Docker to connect through a proxy, you must start the Docker daemon with the
-proper environment variables. The easiest way is to shutdown Docker (e.g. `sudo initctl stop docker`)
-and then run Docker by hand. As root, run:
-
-```sh
-export HTTP_PROXY="http://localhost:9000"
-export HTTPS_PROXY="https://localhost:9000"
-docker daemon --debug
-```
-
-This will launch the Docker daemon and proxy all connections through mitmproxy.
-
-### Running the Docker client
-
-Now that we have mitmproxy and Docker running, we can attempt to login and push
-a container image. You may need to run as root to do this. For example:
-
-```sh
-docker login s3-testing.myregistry.com:4567
-docker push s3-testing.myregistry.com:4567/root/docker-test
-```
-
-In the example above, we see the following trace on the mitmproxy window:
-
-![mitmproxy output from Docker](img/mitmproxy-docker.png)
-
-The above image shows:
-
-* The initial PUT requests went through fine with a 201 status code.
-* The 201 redirected the client to the S3 bucket.
-* The HEAD request to the AWS bucket reported a 403 Unauthorized.
-
-What does this mean? This strongly suggests that the S3 user does not have the right
-[permissions to perform a HEAD request](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html).
-The solution: check the [IAM permissions again](https://docs.docker.com/registry/storage-drivers/s3/).
-Once the right permissions were set, the error will go away.
+This document was moved to [user/project/container_registry](../user/project/container_registry.md).
diff --git a/doc/development/licensing.md b/doc/development/licensing.md
index 8c8c7486fff..05972b33fdb 100644
--- a/doc/development/licensing.md
+++ b/doc/development/licensing.md
@@ -54,6 +54,7 @@ Libraries with the following licenses are acceptable for use:
- [BSD 2-Clause License][BSD-2-Clause]: A permissive (non-copyleft) license as defined by the Open Source Initiative.
- [BSD 3-Clause License][BSD-3-Clause] (also known as New BSD or Modified BSD): A permissive (non-copyleft) license as defined by the Open Source Initiative
- [ISC License][ISC] (also known as the OpenBSD License): A permissive (non-copyleft) license as defined by the Open Source Initiative.
+- [Creative Commons Zero (CC0)][CC0]: A public domain dedication, recommended as a way to disclaim copyright on your work to the maximum extent possible.
## Unacceptable Licenses
@@ -85,6 +86,7 @@ Gems which are included only in the "development" or "test" groups by Bundler ar
[BSD-2-Clause]: https://opensource.org/licenses/BSD-2-Clause
[BSD-3-Clause]: https://opensource.org/licenses/BSD-3-Clause
[ISC]: https://opensource.org/licenses/ISC
+[CC0]: https://creativecommons.org/publicdomain/zero/1.0/
[GPL]: http://choosealicense.com/licenses/gpl-3.0/
[GPLv2]: http://www.gnu.org/licenses/gpl-2.0.txt
[GPLv3]: http://www.gnu.org/licenses/gpl-3.0.txt
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 68ed20ef5bf..378ab6857b8 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -400,7 +400,7 @@ If you are not using Linux you may have to run `gmake` instead of
cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git
cd gitlab-workhorse
- sudo -u git -H git checkout v0.8.2
+ sudo -u git -H git checkout v0.8.4
sudo -u git -H make
### Initialize Database and Activate Advanced Features
diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md
index 411e4837e20..e3f61247be5 100644
--- a/doc/update/8.12-to-8.13.md
+++ b/doc/update/8.12-to-8.13.md
@@ -84,7 +84,7 @@ GitLab 8.1.
```bash
cd /home/git/gitlab-workhorse
sudo -u git -H git fetch --all
-sudo -u git -H git checkout v0.8.2
+sudo -u git -H git checkout v0.8.4
sudo -u git -H make
```
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
new file mode 100644
index 00000000000..b205fea2c40
--- /dev/null
+++ b/doc/user/project/container_registry.md
@@ -0,0 +1,253 @@
+# GitLab Container Registry
+
+> [Introduced][ce-4040] in GitLab 8.8.
+
+---
+
+> **Note**
+Docker Registry manifest `v1` support was added in GitLab 8.9 to support Docker
+versions earlier than 1.10.
+>
+This document is about the user guide. To learn how to enable GitLab Container
+Registry across your GitLab instance, visit the
+[administrator documentation](../../administration/container_registry.md).
+
+With the Docker Container Registry integrated into GitLab, every project can
+have its own space to store its Docker images.
+
+You can read more about Docker Registry at https://docs.docker.com/registry/introduction/.
+
+---
+
+## Enable the Container Registry for your project
+
+1. First, ask your system administrator to enable GitLab Container Registry
+ following the [administration documentation](../../administration/container_registry.md).
+ If you are using GitLab.com, this is enabled by default so you can start using
+ the Registry immediately.
+
+1. Go to your project's settings and enable the **Container Registry** feature
+ on your project. For new projects this might be enabled by default. For
+ existing projects (prior GitLab 8.8), you will have to explicitly enable it.
+
+ ![Enable Container Registry](img/container_registry_enable.png)
+
+1. Hit **Save changes** for the changes to take effect. You should now be able
+ to see the **Registry** link in the project menu.
+
+ ![Container Registry tab](img/container_registry_tab.png)
+
+## Build and push images
+
+If you visit the **Registry** link under your project's menu, you can see the
+explicit instructions to login to the Container Registry using your GitLab
+credentials.
+
+For example if the Registry's URL is `registry.example.com`, the you should be
+able to login with:
+
+```
+docker login registry.example.com
+```
+
+Building and publishing images should be a straightforward process. Just make
+sure that you are using the Registry URL with the namespace and project name
+that is hosted on GitLab:
+
+```
+docker build -t registry.example.com/group/project .
+docker push registry.example.com/group/project
+```
+
+Your image will be named after the following scheme:
+
+```
+<registry URL>/<namespace>/<project>
+```
+
+As such, the name of the image is unique, but you can differentiate the images
+using tags.
+
+## Use images from GitLab Container Registry
+
+To download and run a container from images hosted in GitLab Container Registry,
+use `docker run`:
+
+```
+docker run [options] registry.example.com/group/project [arguments]
+```
+
+For more information on running Docker containers, visit the
+[Docker documentation][docker-docs].
+
+## Control Container Registry from within GitLab
+
+GitLab offers a simple Container Registry management panel. Go to your project
+and click **Registry** in the project menu.
+
+This view will show you all tags in your project and will easily allow you to
+delete them.
+
+![Container Registry panel](img/container_registry_panel.png)
+
+## Build and push images using GitLab CI
+
+> **Note:**
+This feature requires GitLab 8.8 and GitLab Runner 1.2.
+
+Make sure that your GitLab Runner is configured to allow building Docker images by
+following the [Using Docker Build](../ci/docker/using_docker_build.md)
+and [Using the GitLab Container Registry documentation](../ci/docker/using_docker_build.md#using-the-gitlab-container-registry).
+
+## Limitations
+
+In order to use a container image from your private project as an `image:` in
+your `.gitlab-ci.yml`, you have to follow the
+[Using a private Docker Registry][private-docker]
+documentation. This workflow will be simplified in the future.
+
+## Troubleshooting the GitLab Container Registry
+
+### Basic Troubleshooting
+
+1. Check to make sure that the system clock on your Docker client and GitLab server have
+ been synchronized (e.g. via NTP).
+
+2. If you are using an S3-backed Registry, double check that the IAM
+ permissions and the S3 credentials (including region) are correct. See [the
+ sample IAM policy](https://docs.docker.com/registry/storage-drivers/s3/)
+ for more details.
+
+3. Check the Registry logs (e.g. `/var/log/gitlab/registry/current`) and the GitLab production logs
+ for errors (e.g. `/var/log/gitlab/gitlab-rails/production.log`). You may be able to find clues
+ there.
+
+### Advanced Troubleshooting
+
+>**NOTE:** The following section is only recommended for experts.
+
+Sometimes it's not obvious what is wrong, and you may need to dive deeper into
+the communication between the Docker client and the Registry to find out
+what's wrong. We will use a concrete example in the past to illustrate how to
+diagnose a problem with the S3 setup.
+
+#### Unexpected 403 error during push
+
+A user attempted to enable an S3-backed Registry. The `docker login` step went
+fine. However, when pushing an image, the output showed:
+
+```
+The push refers to a repository [s3-testing.myregistry.com:4567/root/docker-test]
+dc5e59c14160: Pushing [==================================================>] 14.85 kB
+03c20c1a019a: Pushing [==================================================>] 2.048 kB
+a08f14ef632e: Pushing [==================================================>] 2.048 kB
+228950524c88: Pushing 2.048 kB
+6a8ecde4cc03: Pushing [==> ] 9.901 MB/205.7 MB
+5f70bf18a086: Pushing 1.024 kB
+737f40e80b7f: Waiting
+82b57dbc5385: Waiting
+19429b698a22: Waiting
+9436069b92a3: Waiting
+error parsing HTTP 403 response body: unexpected end of JSON input: ""
+```
+
+This error is ambiguous, as it's not clear whether the 403 is coming from the
+GitLab Rails application, the Docker Registry, or something else. In this
+case, since we know that since the login succeeded, we probably need to look
+at the communication between the client and the Registry.
+
+The REST API between the Docker client and Registry is [described
+here](https://docs.docker.com/registry/spec/api/). Normally, one would just
+use Wireshark or tcpdump to capture the traffic and see where things went
+wrong. However, since all communication between Docker clients and servers
+are done over HTTPS, it's a bit difficult to decrypt the traffic quickly even
+if you know the private key. What can we do instead?
+
+One way would be to disable HTTPS by setting up an [insecure
+Registry](https://docs.docker.com/registry/insecure/). This could introduce a
+security hole and is only recommended for local testing. If you have a
+production system and can't or don't want to do this, there is another way:
+use mitmproxy, which stands for Man-in-the-Middle Proxy.
+
+#### mitmproxy
+
+[mitmproxy](https://mitmproxy.org/) allows you to place a proxy between your
+client and server to inspect all traffic. One wrinkle is that your system
+needs to trust the mitmproxy SSL certificates for this to work.
+
+The following installation instructions assume you are running Ubuntu:
+
+1. Install mitmproxy (see http://docs.mitmproxy.org/en/stable/install.html)
+1. Run `mitmproxy --port 9000` to generate its certificates.
+ Enter <kbd>CTRL</kbd>-<kbd>C</kbd> to quit.
+1. Install the certificate from `~/.mitmproxy` to your system:
+
+ ```sh
+ sudo cp ~/.mitmproxy/mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy-ca-cert.crt
+ sudo update-ca-certificates
+ ```
+
+If successful, the output should indicate that a certificate was added:
+
+```sh
+Updating certificates in /etc/ssl/certs... 1 added, 0 removed; done.
+Running hooks in /etc/ca-certificates/update.d....done.
+```
+
+To verify that the certificates are properly installed, run:
+
+```sh
+mitmproxy --port 9000
+```
+
+This will run mitmproxy on port `9000`. In another window, run:
+
+```sh
+curl --proxy http://localhost:9000 https://httpbin.org/status/200
+```
+
+If everything is setup correctly, you will see information on the mitmproxy window and
+no errors from the curl commands.
+
+#### Running the Docker daemon with a proxy
+
+For Docker to connect through a proxy, you must start the Docker daemon with the
+proper environment variables. The easiest way is to shutdown Docker (e.g. `sudo initctl stop docker`)
+and then run Docker by hand. As root, run:
+
+```sh
+export HTTP_PROXY="http://localhost:9000"
+export HTTPS_PROXY="https://localhost:9000"
+docker daemon --debug
+```
+
+This will launch the Docker daemon and proxy all connections through mitmproxy.
+
+#### Running the Docker client
+
+Now that we have mitmproxy and Docker running, we can attempt to login and push
+a container image. You may need to run as root to do this. For example:
+
+```sh
+docker login s3-testing.myregistry.com:4567
+docker push s3-testing.myregistry.com:4567/root/docker-test
+```
+
+In the example above, we see the following trace on the mitmproxy window:
+
+![mitmproxy output from Docker](img/mitmproxy-docker.png)
+
+The above image shows:
+
+* The initial PUT requests went through fine with a 201 status code.
+* The 201 redirected the client to the S3 bucket.
+* The HEAD request to the AWS bucket reported a 403 Unauthorized.
+
+What does this mean? This strongly suggests that the S3 user does not have the right
+[permissions to perform a HEAD request](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html).
+The solution: check the [IAM permissions again](https://docs.docker.com/registry/storage-drivers/s3/).
+Once the right permissions were set, the error will go away.
+
+[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040
+[docker-docs]: https://docs.docker.com/engine/userguide/intro/
+[private-docker]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#using-a-private-docker-registry
diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md
index abef80e7914..c16058165d7 100644
--- a/doc/user/project/cycle_analytics.md
+++ b/doc/user/project/cycle_analytics.md
@@ -6,7 +6,7 @@
This the first iteration of Cycle Analytics, you can follow the following issue
to track the changes that are coming to this feature: [#20975][ce-20975].
-Cycle Analytics measures the time it takes to go from [an idea to production] for
+Cycle Analytics measures the time it takes to go from an [idea to production] for
each project you have. This is achieved by not only indicating the total time it
takes to reach at that point, but the total time is broken down into the
multiple stages an idea has to pass through to be shipped.
@@ -28,9 +28,10 @@ You can see that there are seven stages in total:
(first assignment, any milestone, milestone date or assignee is not required)
- **Plan** (Board)
- Median time from giving an issue a milestone or label until pushing the
- first commit
+ first commit to the branch
- **Code** (IDE)
- - Median time from the first commit until the merge request is created
+ - Median time from the first commit to the branch until the merge request is
+ created
- **Test** (CI)
- Median total test time for all commits/merges
- **Review** (Merge Request/MR)
@@ -40,7 +41,10 @@ You can see that there are seven stages in total:
- Median time from when the merge request got merged until the deploy to
production (production is last stage/environment)
- **Production** (Total)
- - Sum of all the above stages excluding the Test (CI) time
+ - Sum of all the above stages' times excluding the Test (CI) time. To clarify,
+ it's not so much that CI time is "excluded", but rather CI time is already
+ counted in the review stage since CI is done automatically. Most of the
+ other stages are purely sequential, but **Test** is not.
## How the data is measured
@@ -57,25 +61,24 @@ Below you can see in more detail what the various stages of Cycle Analytics mean
| **Stage** | **Description** |
| --------- | --------------- |
| Issue | Measures the median time between creating an issue and taking action to solve it, by either labeling it or adding it to a milestone, whatever comes first. The label will be tracked only if it already has an [Issue Board list][board] created for it. |
-| Plan | Measures the median time between the action you took for the previous stage, and pushing the first commit to the repository. To make this change tracked, the pushed commit needs to contain the [issue closing pattern], for example `Closes #xxx`, where `xxx` is the number of the issue related to this commit. If the commit does not contain the issue closing pattern, it is not considered to the measurement time of the stage. |
-| Code | Measures the median time between pushing a first commit (previous stage) and creating a merge request related to that commit. The key to keep the process tracked is include the [issue closing pattern] to the description of the merge request. |
+| Plan | Measures the median time between the action you took for the previous stage, and pushing the first commit to the branch. The very first commit of the branch is the one that triggers the separation between **Plan** and **Code**, and at least one of the commits in the branch needs to contain the related issue number (e.g., `#42`). If none of the commits in the branch mention the related issue number, it is not considered to the measurement time of the stage. |
+| Code | Measures the median time between pushing a first commit (previous stage) and creating a merge request (MR) related to that commit. The key to keep the process tracked is to include the [issue closing pattern] to the description of the merge request (for example, `Closes #xxx`, where `xxx` is the number of the issue related to this merge request). If the issue closing pattern is not present in the merge request description, the MR is not considered to the measurement time of the stage. |
| Test | Measures the median time to run the entire pipeline for that project. It's related to the time GitLab CI takes to run every job for the commits pushed to that merge request defined in the previous stage. It is basically the start->finish time for all pipelines. `master` is not excluded. It does not attempt to track time for any particular stages. |
| Review | Measures the median time taken to review the merge request, between its creation and until it's merged. |
-| Staging | Measures the median time between merging the merge request until the very first deployment to production. It's tracked by the [environment] set to `production` in your GitLab CI configuration. If there isn't a `production` environment, this is not tracked. |
-| Production| The sum of all time taken to run the entire process, from issue creation to deploying the code to production. |
+| Staging | Measures the median time between merging the merge request until the very first deployment to production. It's tracked by the [environment] set to `production` (case-sensitive, `Production` won't work) in your GitLab CI configuration. If there isn't a `production` environment, this is not tracked. |
+| Production| The sum of all time (medians) taken to run the entire process, from issue creation to deploying the code to production. |
---
Here's a little explanation of how this works behind the scenes:
1. Issues and merge requests are grouped together in pairs, such that for each
- `<issue, merge request>` pair, the merge request has `Fixes #xxx` for the
- corresponding issue. All other issues and merge requests are **not** considered.
-
+ `<issue, merge request>` pair, the merge request has the [issue closing pattern]
+ for the corresponding issue. All other issues and merge requests are **not**
+ considered.
1. Then the <issue, merge request> pairs are filtered out. Any merge request
that has **not** been deployed to production in the last XX days (specified
by the UI - default is 90 days) prohibits these pairs from being considered.
-
1. For the remaining `<issue, merge request>` pairs, we check the information that
we need for the stages, like issue creation date, merge request merge time,
etc.
@@ -86,6 +89,60 @@ label present in the Issue Board or assigned a milestone or a project has no
`production` environment, the Cycle Analytics dashboard won't present any data
at all.
+## Example workflow
+
+Below is a simple fictional workflow of a single cycle that happens in a
+single day passing through all seven stages. Note that if a stage does not have
+a start/stop mark, it is not measured and hence not calculated in the median
+time. It is assumed that milestones are created and CI for testing and setting
+environments is configured.
+
+1. Issue is created at 09:00 (start of **Issue** stage).
+1. Issue is added to a milestone at 11:00 (stop of **Issue** stage / start of
+ **Plan** stage).
+1. Start working on the issue, create a branch locally and make one commit at
+ 12:00.
+1. Make a second commit to the branch which mentions the issue number at 12.30
+ (stop of **Plan** stage / start of **Code** stage).
+1. Push branch and create a merge request that contains the [issue closing pattern]
+ in its description at 14:00 (stop of **Code** stage / start of **Test** and
+ **Review** stages).
+1. The CI starts running your scripts defined in [`.gitlab-ci.yml`][yml] and
+ takes 5min (stop of **Test** stage).
+1. Review merge request, ensure that everything is OK and merge the merge
+ request at 19:00. (stop of **Review** stage / start of **Staging** stage).
+1. Now that the merge request is merged, a deployment to the `production`
+ environment starts and finishes at 19:30 (stop of **Staging** stage).
+1. The cycle completes and the sum of the median times of the previous stages
+ is recorded to the **Production** stage. That is the time between creating an
+ issue and deploying its relevant merge request to production.
+
+From the above example you can conclude the time it took each stage to complete
+as long as their total time:
+
+- **Issue**: 2h (11:00 - 09:00)
+- **Plan**: 1h (12:00 - 11:00)
+- **Code**: 2h (14:00 - 12:00)
+- **Test**: 5min
+- **Review**: 5h (19:00 - 14:00)
+- **Staging**: 30min (19:30 - 19:00)
+- **Production**: Since this stage measures the sum of median time off all
+ previous stages, we cannot calculate it if we don't know the status of the
+ stages before. In case this is the very first cycle that is run in the project,
+ then the **Production** time is 10h 30min (19:30 - 09:00)
+
+A few notes:
+
+- In the above example we demonstrated that it doesn't matter if your first
+ commit doesn't mention the issue number, you can do this later in any commit
+ of the branch you are working on.
+- You can see that the **Test** stage is not calculated to the overall time of
+ the cycle since it is included in the **Review** process (every MR should be
+ tested).
+- The example above was just **one cycle** of the seven stages. Add multiple
+ cycles, calculate their median time and the result is what the dashboard of
+ Cycle Analytics is showing.
+
## Permissions
The current permissions on the Cycle Analytics dashboard are:
@@ -104,11 +161,12 @@ Learn more about Cycle Analytics in the following resources:
- [Cycle Analytics feature highlight](https://about.gitlab.com/2016/09/21/cycle-analytics-feature-highlight/)
+[board]: issue_board.md#creating-a-new-list
[ce-5986]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5986
[ce-20975]: https://gitlab.com/gitlab-org/gitlab-ce/issues/20975
-[GitLab flow]: ../../workflow/gitlab_flow.md
-[permissions]: ../permissions.md
[environment]: ../../ci/yaml/README.md#environment
-[board]: issue_board.md#creating-a-new-list
+[GitLab flow]: ../../workflow/gitlab_flow.md
[idea to production]: https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab
[issue closing pattern]: issues/automatic_issue_closing.md
+[permissions]: ../permissions.md
+[yml]: ../../ci/yaml/README.md
diff --git a/doc/user/project/img/container_registry_enable.png b/doc/user/project/img/container_registry_enable.png
new file mode 100644
index 00000000000..6fffa2a91d8
--- /dev/null
+++ b/doc/user/project/img/container_registry_enable.png
Binary files differ
diff --git a/doc/user/project/img/container_registry_panel.png b/doc/user/project/img/container_registry_panel.png
new file mode 100644
index 00000000000..60fd76192b7
--- /dev/null
+++ b/doc/user/project/img/container_registry_panel.png
Binary files differ
diff --git a/doc/user/project/img/container_registry_tab.png b/doc/user/project/img/container_registry_tab.png
new file mode 100644
index 00000000000..36b883aaa97
--- /dev/null
+++ b/doc/user/project/img/container_registry_tab.png
Binary files differ
diff --git a/doc/user/project/img/cycle_analytics_landing_page.png b/doc/user/project/img/cycle_analytics_landing_page.png
index 4fa42c87395..b212134d5ed 100644
--- a/doc/user/project/img/cycle_analytics_landing_page.png
+++ b/doc/user/project/img/cycle_analytics_landing_page.png
Binary files differ
diff --git a/doc/container_registry/img/mitmproxy-docker.png b/doc/user/project/img/mitmproxy-docker.png
index 4e3e37b413d..4e3e37b413d 100644
--- a/doc/container_registry/img/mitmproxy-docker.png
+++ b/doc/user/project/img/mitmproxy-docker.png
Binary files differ
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index cac926b3e28..4a6c0d88241 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -31,9 +31,10 @@ Below is a table of the definitions used for GitLab's Issue Board.
There are three types of lists, the ones you create based on your labels, and
two default:
-- **Backlog** (default): shows all opened issues that do not fall in one of the other lists. Always appears on the very left.
-- **Done** (default): shows all closed issues that do not fall in one of the other lists. Always appears on the very right.
-- Label list: a list based on a label. It shows all opened or closed issues with that label.
+- **Backlog** (default): shows all issues that do not fall in one of the other lists. Always appears on the very left.
+- **Done** (default): shows all closed issues. Always appears on the very right.
+Label list: a list based on a label. It shows all issues with that label.
+- Label list: a list based on a label. It shows all opened issues with that label.
![GitLab Issue Board](img/issue_board.png)
diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature
index 8b0cb90765e..1776c07e60e 100644
--- a/features/project/commits/commits.feature
+++ b/features/project/commits/commits.feature
@@ -37,6 +37,11 @@ Feature: Project Commits
Then I see commit info
And I see side-by-side diff button
+ Scenario: I browse commit from list and create a new tag
+ Given I click on commit link
+ And I click on tag link
+ Then I see commit SHA pre-filled
+
Scenario: I browse commit with ci from list
Given commit has ci status
And repository contains ".gitlab-ci.yml" file
diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature
index fdffd71de85..d4b91fec6e8 100644
--- a/features/project/source/browse_files.feature
+++ b/features/project/source/browse_files.feature
@@ -71,6 +71,7 @@ Feature: Project Source Browse Files
And I fill the new branch name
And I click on "Commit Changes"
Then I am redirected to the new merge request page
+ When I click on "Changes" tab
And I should see its new content
@javascript
@@ -80,9 +81,10 @@ Feature: Project Source Browse Files
And I fill the upload file commit message
And I fill the new branch name
And I click on "Upload file"
- Then I can see the new text file
+ Then I can see the new commit message
And I am redirected to the new merge request page
- And I can see the new commit message
+ When I click on "Changes" tab
+ Then I can see the new text file
@javascript
Scenario: I can upload file and commit when I don't have write access
@@ -93,9 +95,10 @@ Feature: Project Source Browse Files
And I upload a new text file
And I fill the upload file commit message
And I click on "Upload file"
- Then I can see the new text file
+ Then I can see the new commit message
And I am redirected to the fork's new merge request page
- And I can see the new commit message
+ When I click on "Changes" tab
+ Then I can see the new text file
@javascript
Scenario: I can replace file and commit
@@ -119,9 +122,10 @@ Feature: Project Source Browse Files
And I replace it with a text file
And I fill the replace file commit message
And I click on "Replace file"
- Then I can see the new text file
- And I am redirected to the fork's new merge request page
And I can see the replacement commit message
+ And I am redirected to the fork's new merge request page
+ When I click on "Changes" tab
+ Then I can see the new text file
@javascript
Scenario: If I enter an illegal file name I see an error message
@@ -191,6 +195,7 @@ Feature: Project Source Browse Files
And I fill the new branch name
And I click on "Commit Changes"
Then I am redirected to the new merge request page
+ Then I click on "Changes" tab
And I should see its new content
@javascript @wip
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index bea9f9d198b..b8264f97687 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -24,6 +24,14 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
expect(body).to have_selector("entry summary", text: commit.description[0..10])
end
+ step 'I click on tag link' do
+ click_link "Tag"
+ end
+
+ step 'I see commit SHA pre-filled' do
+ expect(page).to have_selector("input[value='#{sample_commit.id}']")
+ end
+
step 'I click on commit link' do
visit namespace_project_commit_path(@project.namespace, @project, sample_commit.id)
end
diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb
index 8abeb5ee242..70dbd030003 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -70,6 +70,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
step 'There is an existent fork of the "Shop" project' do
user = create(:user, name: 'Mike')
+ @project.team << [user, :reporter]
@forked_project = Projects::ForkService.new(@project, user).execute
end
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index bb79424ee08..1cc9e37b075 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -105,6 +105,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
click_button 'Commit Changes'
end
+ step 'I click on "Changes" tab' do
+ click_link 'Changes'
+ end
+
step 'I click on "Create directory"' do
click_button 'Create directory'
end
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
index d3db7740830..87915b19480 100644
--- a/lib/api/access_requests.rb
+++ b/lib/api/access_requests.rb
@@ -5,15 +5,14 @@ module API
helpers ::API::Helpers::MembersHelpers
%w[group project].each do |source_type|
+ params do
+ requires :id, type: String, desc: "The #{source_type} ID"
+ end
resource source_type.pluralize do
- # Get a list of group/project access requests viewable by the authenticated user.
- #
- # Parameters:
- # id (required) - The group/project ID
- #
- # Example Request:
- # GET /groups/:id/access_requests
- # GET /projects/:id/access_requests
+ desc "Gets a list of access requests for a #{source_type}." do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::AccessRequester
+ end
get ":id/access_requests" do
source = find_source(source_type, params[:id])
@@ -23,14 +22,10 @@ module API
present access_requesters.map(&:user), with: Entities::AccessRequester, source: source
end
- # Request access to the group/project
- #
- # Parameters:
- # id (required) - The group/project ID
- #
- # Example Request:
- # POST /groups/:id/access_requests
- # POST /projects/:id/access_requests
+ desc "Requests access for the authenticated user to a #{source_type}." do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::AccessRequester
+ end
post ":id/access_requests" do
source = find_source(source_type, params[:id])
access_requester = source.request_access(current_user)
@@ -42,37 +37,30 @@ module API
end
end
- # Approve a group/project access request
- #
- # Parameters:
- # id (required) - The group/project ID
- # user_id (required) - The user ID of the access requester
- # access_level (optional) - Access level
- #
- # Example Request:
- # PUT /groups/:id/access_requests/:user_id/approve
- # PUT /projects/:id/access_requests/:user_id/approve
+ desc 'Approves an access request for the given user.' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Member
+ end
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the access requester'
+ optional :access_level, type: Integer, desc: 'A valid access level (defaults: `30`, developer access level)'
+ end
put ':id/access_requests/:user_id/approve' do
- required_attributes! [:user_id]
source = find_source(source_type, params[:id])
- member = ::Members::ApproveAccessRequestService.new(source, current_user, params).execute
+ member = ::Members::ApproveAccessRequestService.new(source, current_user, declared(params)).execute
status :created
present member.user, with: Entities::Member, member: member
end
- # Deny a group/project access request
- #
- # Parameters:
- # id (required) - The group/project ID
- # user_id (required) - The user ID of the access requester
- #
- # Example Request:
- # DELETE /groups/:id/access_requests/:user_id
- # DELETE /projects/:id/access_requests/:user_id
+ desc 'Denies an access request for the given user.' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ end
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the access requester'
+ end
delete ":id/access_requests/:user_id" do
- required_attributes! [:user_id]
source = find_source(source_type, params[:id])
::Members::DestroyService.new(source, current_user, params).
diff --git a/lib/api/api.rb b/lib/api/api.rb
index cb47ec8f33f..0bbf73a1b63 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -43,6 +43,7 @@ module API
mount ::API::Groups
mount ::API::Internal
mount ::API::Issues
+ mount ::API::Boards
mount ::API::Keys
mount ::API::Labels
mount ::API::LicenseTemplates
diff --git a/lib/api/boards.rb b/lib/api/boards.rb
new file mode 100644
index 00000000000..4d5d144a02e
--- /dev/null
+++ b/lib/api/boards.rb
@@ -0,0 +1,115 @@
+module API
+ # Boards API
+ class Boards < Grape::API
+ before { authenticate! }
+
+ resource :projects do
+ # Get the project board
+ get ':id/boards' do
+ authorize!(:read_board, user_project)
+ present [user_project.board], with: Entities::Board
+ end
+
+ segment ':id/boards/:board_id' do
+ helpers do
+ def project_board
+ board = user_project.board
+ if params[:board_id].to_i == board.id
+ board
+ else
+ not_found!('Board')
+ end
+ end
+
+ def board_lists
+ project_board.lists.destroyable
+ end
+ end
+
+ # Get the lists of a project board
+ # Does not include `backlog` and `done` lists
+ get '/lists' do
+ authorize!(:read_board, user_project)
+ present board_lists, with: Entities::List
+ end
+
+ # Get a list of a project board
+ get '/lists/:list_id' do
+ authorize!(:read_board, user_project)
+ present board_lists.find(params[:list_id]), with: Entities::List
+ end
+
+ # Create a new board list
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # label_id (required) - The ID of an existing label
+ # Example Request:
+ # POST /projects/:id/boards/:board_id/lists
+ post '/lists' do
+ required_attributes! [:label_id]
+
+ unless user_project.labels.exists?(params[:label_id])
+ render_api_error!({ error: "Label not found!" }, 400)
+ end
+
+ authorize!(:admin_list, user_project)
+
+ list = ::Boards::Lists::CreateService.new(user_project, current_user,
+ { label_id: params[:label_id] }).execute
+
+ if list.valid?
+ present list, with: Entities::List
+ else
+ render_validation_error!(list)
+ end
+ end
+
+ # Moves a board list to a new position
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # board_id (required) - The ID of a board
+ # position (required) - The position of the list
+ # Example Request:
+ # PUT /projects/:id/boards/:board_id/lists/:list_id
+ put '/lists/:list_id' do
+ list = project_board.lists.movable.find(params[:list_id])
+
+ authorize!(:admin_list, user_project)
+
+ moved = ::Boards::Lists::MoveService.new(user_project, current_user,
+ { position: params[:position].to_i }).execute(list)
+
+ if moved
+ present list, with: Entities::List
+ else
+ render_api_error!({ error: "List could not be moved!" }, 400)
+ end
+ end
+
+ # Delete a board list
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # board_id (required) - The ID of a board
+ # list_id (required) - The ID of a board list
+ # Example Request:
+ # DELETE /projects/:id/boards/:board_id/lists/:list_id
+ delete "/lists/:list_id" do
+ list = board_lists.find_by(id: params[:list_id])
+
+ authorize!(:admin_list, user_project)
+
+ if list
+ destroyed_list = ::Boards::Lists::DestroyService.new(
+ user_project, current_user).execute(list)
+ present destroyed_list, with: Entities::List
+ else
+ not_found!('List')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index b4eaf1813d4..14ddc8c9a62 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -29,6 +29,42 @@ module API
present commits, with: Entities::RepoCommit
end
+ desc 'Commit multiple file changes as one commit' do
+ detail 'This feature was introduced in GitLab 8.13'
+ end
+
+ params do
+ requires :id, type: Integer, desc: 'The project ID'
+ requires :branch_name, type: String, desc: 'The name of branch'
+ requires :commit_message, type: String, desc: 'Commit message'
+ requires :actions, type: Array, desc: 'Actions to perform in commit'
+ optional :author_email, type: String, desc: 'Author email for commit'
+ optional :author_name, type: String, desc: 'Author name for commit'
+ end
+
+ post ":id/repository/commits" do
+ authorize! :push_code, user_project
+
+ attrs = declared(params)
+ attrs[:source_branch] = attrs[:branch_name]
+ attrs[:target_branch] = attrs[:branch_name]
+ attrs[:actions].map! do |action|
+ action[:action] = action[:action].to_sym
+ action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/')
+ action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/')
+ action
+ end
+
+ result = ::Files::MultiService.new(user_project, current_user, attrs).execute
+
+ if result[:status] == :success
+ commit_detail = user_project.repository.commits(result[:result], limit: 1).first
+ present commit_detail, with: Entities::RepoCommitDetail
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
+
# Get a specific commit of a project
#
# Parameters:
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 04437322ec1..feaa0c213bf 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -432,8 +432,11 @@ module API
end
end
- class Label < Grape::Entity
+ class LabelBasic < Grape::Entity
expose :name, :color, :description
+ end
+
+ class Label < LabelBasic
expose :open_issues_count, :closed_issues_count, :open_merge_requests_count
expose :subscribed do |label, options|
@@ -441,6 +444,19 @@ module API
end
end
+ class List < Grape::Entity
+ expose :id
+ expose :label, using: Entities::LabelBasic
+ expose :position
+ end
+
+ class Board < Grape::Entity
+ expose :id
+ expose :lists, using: Entities::List do |board|
+ board.lists.destroyable
+ end
+ end
+
class Compare < Grape::Entity
expose :commit, using: Entities::RepoCommit do |compare, options|
Commit.decorate(compare.commits, nil).last
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 714d4ea3dc6..8b8c4eb4d46 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -21,8 +21,11 @@ module API
end
# Check the Rails session for valid authentication details
+ #
+ # Until CSRF protection is added to the API, disallow this method for
+ # state-changing endpoints
def find_user_from_warden
- warden ? warden.authenticate : nil
+ warden.try(:authenticate) if request.get? || request.head?
end
def find_user_by_private_token
diff --git a/lib/api/members.rb b/lib/api/members.rb
index 34df55fe192..b80818f0eb6 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -5,16 +5,16 @@ module API
helpers ::API::Helpers::MembersHelpers
%w[group project].each do |source_type|
+ params do
+ requires :id, type: String, desc: "The #{source_type} ID"
+ end
resource source_type.pluralize do
- # Get a list of group/project members viewable by the authenticated user.
- #
- # Parameters:
- # id (required) - The group/project ID
- # query - Query string
- #
- # Example Request:
- # GET /groups/:id/members
- # GET /projects/:id/members
+ desc 'Gets a list of group or project members viewable by the authenticated user.' do
+ success Entities::Member
+ end
+ params do
+ optional :query, type: String, desc: 'A query string to search for members'
+ end
get ":id/members" do
source = find_source(source_type, params[:id])
@@ -25,15 +25,12 @@ module API
present users, with: Entities::Member, source: source
end
- # Get a group/project member
- #
- # Parameters:
- # id (required) - The group/project ID
- # user_id (required) - The user ID of the member
- #
- # Example Request:
- # GET /groups/:id/members/:user_id
- # GET /projects/:id/members/:user_id
+ desc 'Gets a member of a group or project.' do
+ success Entities::Member
+ end
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the member'
+ end
get ":id/members/:user_id" do
source = find_source(source_type, params[:id])
@@ -43,26 +40,25 @@ module API
present member.user, with: Entities::Member, member: member
end
- # Add a new group/project member
- #
- # Parameters:
- # id (required) - The group/project ID
- # user_id (required) - The user ID of the new member
- # access_level (required) - A valid access level
- # expires_at (optional) - Date string in the format YEAR-MONTH-DAY
- #
- # Example Request:
- # POST /groups/:id/members
- # POST /projects/:id/members
+ desc 'Adds a member to a group or project.' do
+ success Entities::Member
+ end
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the new member'
+ requires :access_level, type: Integer, desc: 'A valid access level (defaults: `30`, developer access level)'
+ optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
+ end
post ":id/members" do
source = find_source(source_type, params[:id])
authorize_admin_source!(source_type, source)
- required_attributes! [:user_id, :access_level]
member = source.members.find_by(user_id: params[:user_id])
- # This is to ensure back-compatibility but 409 behavior should be used
- # for both project and group members in 9.0!
+ # We need this explicit check because `source.add_user` doesn't
+ # currently return the member created so it would return 201 even if
+ # the member already existed...
+ # The `source_type == 'group'` check is to ensure back-compatibility
+ # but 409 behavior should be used for both project and group members in 9.0!
conflict!('Member already exists') if source_type == 'group' && member
unless member
@@ -79,21 +75,17 @@ module API
end
end
- # Update a group/project member
- #
- # Parameters:
- # id (required) - The group/project ID
- # user_id (required) - The user ID of the member
- # access_level (required) - A valid access level
- # expires_at (optional) - Date string in the format YEAR-MONTH-DAY
- #
- # Example Request:
- # PUT /groups/:id/members/:user_id
- # PUT /projects/:id/members/:user_id
+ desc 'Updates a member of a group or project.' do
+ success Entities::Member
+ end
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the new member'
+ requires :access_level, type: Integer, desc: 'A valid access level'
+ optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
+ end
put ":id/members/:user_id" do
source = find_source(source_type, params[:id])
authorize_admin_source!(source_type, source)
- required_attributes! [:user_id, :access_level]
member = source.members.find_by!(user_id: params[:user_id])
attrs = attributes_for_keys [:access_level, :expires_at]
@@ -108,18 +100,12 @@ module API
end
end
- # Remove a group/project member
- #
- # Parameters:
- # id (required) - The group/project ID
- # user_id (required) - The user ID of the member
- #
- # Example Request:
- # DELETE /groups/:id/members/:user_id
- # DELETE /projects/:id/members/:user_id
+ desc 'Removes a user from a group or project.'
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the member'
+ end
delete ":id/members/:user_id" do
source = find_source(source_type, params[:id])
- required_attributes! [:user_id]
# This is to ensure back-compatibility but find_by! should be used
# in that casse in 9.0!
@@ -134,7 +120,7 @@ module API
if member.nil?
{ message: "Access revoked", id: params[:user_id].to_i }
else
- ::Members::DestroyService.new(source, current_user, params).execute
+ ::Members::DestroyService.new(source, current_user, declared(params)).execute
present member.user, with: Entities::Member, member: member
end
diff --git a/lib/constraints/group_url_constrainer.rb b/lib/constraints/group_url_constrainer.rb
new file mode 100644
index 00000000000..ca39b1961ae
--- /dev/null
+++ b/lib/constraints/group_url_constrainer.rb
@@ -0,0 +1,7 @@
+require 'constraints/namespace_url_constrainer'
+
+class GroupUrlConstrainer < NamespaceUrlConstrainer
+ def find_resource(id)
+ Group.find_by_path(id)
+ end
+end
diff --git a/lib/constraints/namespace_url_constrainer.rb b/lib/constraints/namespace_url_constrainer.rb
new file mode 100644
index 00000000000..23920193743
--- /dev/null
+++ b/lib/constraints/namespace_url_constrainer.rb
@@ -0,0 +1,13 @@
+class NamespaceUrlConstrainer
+ def matches?(request)
+ id = request.path.sub(/\A\/+/, '').split('/').first.sub(/.atom\z/, '')
+
+ if id =~ Gitlab::Regex.namespace_regex
+ find_resource(id)
+ end
+ end
+
+ def find_resource(id)
+ Namespace.find_by_path(id)
+ end
+end
diff --git a/lib/constraints/user_url_constrainer.rb b/lib/constraints/user_url_constrainer.rb
new file mode 100644
index 00000000000..504a0f5d93e
--- /dev/null
+++ b/lib/constraints/user_url_constrainer.rb
@@ -0,0 +1,7 @@
+require 'constraints/namespace_url_constrainer'
+
+class UserUrlConstrainer < NamespaceUrlConstrainer
+ def find_resource(id)
+ User.find_by('lower(username) = ?', id.downcase)
+ end
+end
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index e33ac61f5ae..7f424b74efb 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -102,9 +102,19 @@ module Gitlab
def request(method, *args, &block)
sleep rate_limit_sleep_time if rate_limit_exceed?
- data = api.send(method, *args, &block)
- yield data
+ data = api.send(method, *args)
+ return data unless data.is_a?(Array)
+ if block_given?
+ yield data
+ each_response_page(&block)
+ else
+ each_response_page { |page| data.concat(page) }
+ data
+ end
+ end
+
+ def each_response_page
last_response = api.last_response
while last_response.rels[:next]
diff --git a/lib/gitlab/import_export/attribute_cleaner.rb b/lib/gitlab/import_export/attribute_cleaner.rb
new file mode 100644
index 00000000000..b9e4042220a
--- /dev/null
+++ b/lib/gitlab/import_export/attribute_cleaner.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module ImportExport
+ class AttributeCleaner
+ ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES
+
+ def self.clean!(relation_hash:)
+ relation_hash.reject! do |key, _value|
+ key.end_with?('_id') && !ALLOWED_REFERENCES.include?(key)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb
index e522a0fc8f6..f00c7460e82 100644
--- a/lib/gitlab/import_export/command_line_util.rb
+++ b/lib/gitlab/import_export/command_line_util.rb
@@ -1,6 +1,8 @@
module Gitlab
module ImportExport
module CommandLineUtil
+ DEFAULT_MODE = 0700
+
def tar_czf(archive:, dir:)
tar_with_options(archive: archive, dir: dir, options: 'czf')
end
@@ -21,6 +23,11 @@ module Gitlab
execute(%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args)
end
+ def mkdir_p(path)
+ FileUtils.mkdir_p(path, mode: DEFAULT_MODE)
+ FileUtils.chmod(DEFAULT_MODE, path)
+ end
+
private
def tar_with_options(archive:, dir:, options:)
@@ -45,7 +52,7 @@ module Gitlab
# if we are copying files, create the destination folder
destination_folder = File.file?(source) ? File.dirname(destination) : destination
- FileUtils.mkdir_p(destination_folder)
+ mkdir_p(destination_folder)
FileUtils.copy_entry(source, destination)
true
end
diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb
index eca6e5b6d51..113895ba22c 100644
--- a/lib/gitlab/import_export/file_importer.rb
+++ b/lib/gitlab/import_export/file_importer.rb
@@ -15,7 +15,7 @@ module Gitlab
end
def import
- FileUtils.mkdir_p(@shared.export_path)
+ mkdir_p(@shared.export_path)
wait_for_archived_file do
decompress_archive
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index 35ff134ea19..5a109f24f9f 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -110,9 +110,10 @@ module Gitlab
def create_relation(relation, relation_hash_list)
relation_array = [relation_hash_list].flatten.map do |relation_hash|
Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym,
- relation_hash: relation_hash.merge('project_id' => restored_project.id),
+ relation_hash: relation_hash,
members_mapper: members_mapper,
- user: @user)
+ user: @user,
+ project_id: restored_project.id)
end
relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb
index 9153088e966..2fbf437ec26 100644
--- a/lib/gitlab/import_export/project_tree_saver.rb
+++ b/lib/gitlab/import_export/project_tree_saver.rb
@@ -1,6 +1,8 @@
module Gitlab
module ImportExport
class ProjectTreeSaver
+ include Gitlab::ImportExport::CommandLineUtil
+
attr_reader :full_path
def initialize(project:, shared:)
@@ -10,7 +12,7 @@ module Gitlab
end
def save
- FileUtils.mkdir_p(@shared.export_path)
+ mkdir_p(@shared.export_path)
File.write(full_path, project_json_tree)
true
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 354ccd64696..9300f789e1b 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -13,6 +13,8 @@ module Gitlab
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze
+ PROJECT_REFERENCES = %w[project_id source_project_id gl_project_id target_project_id].freeze
+
BUILD_MODELS = %w[Ci::Build commit_status].freeze
IMPORTED_OBJECT_MAX_RETRIES = 5.freeze
@@ -25,9 +27,9 @@ module Gitlab
new(*args).create
end
- def initialize(relation_sym:, relation_hash:, members_mapper:, user:)
+ def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project_id:)
@relation_name = OVERRIDES[relation_sym] || relation_sym
- @relation_hash = relation_hash.except('id', 'noteable_id')
+ @relation_hash = relation_hash.except('id', 'noteable_id').merge('project_id' => project_id)
@members_mapper = members_mapper
@user = user
@imported_object_retries = 0
@@ -153,7 +155,11 @@ module Gitlab
end
def parsed_relation_hash
- @parsed_relation_hash ||= @relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) }
+ @parsed_relation_hash ||= begin
+ Gitlab::ImportExport::AttributeCleaner.clean!(relation_hash: @relation_hash)
+
+ @relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) }
+ end
end
def set_st_diffs
diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb
index d1e33ea8678..48a9a6fa5e2 100644
--- a/lib/gitlab/import_export/repo_restorer.rb
+++ b/lib/gitlab/import_export/repo_restorer.rb
@@ -12,7 +12,7 @@ module Gitlab
def restore
return true unless File.exist?(@path_to_bundle)
- FileUtils.mkdir_p(path_to_repo)
+ mkdir_p(path_to_repo)
git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) && repo_restore_hooks
rescue => e
diff --git a/lib/gitlab/import_export/repo_saver.rb b/lib/gitlab/import_export/repo_saver.rb
index 331e14021e6..a7028a32570 100644
--- a/lib/gitlab/import_export/repo_saver.rb
+++ b/lib/gitlab/import_export/repo_saver.rb
@@ -20,7 +20,7 @@ module Gitlab
private
def bundle_to_disk
- FileUtils.mkdir_p(@shared.export_path)
+ mkdir_p(@shared.export_path)
git_bundle(repo_path: path_to_repo, bundle_path: @full_path)
rescue => e
@shared.error(e)
diff --git a/lib/gitlab/import_export/version_saver.rb b/lib/gitlab/import_export/version_saver.rb
index 9b642d740b7..7cf88298642 100644
--- a/lib/gitlab/import_export/version_saver.rb
+++ b/lib/gitlab/import_export/version_saver.rb
@@ -1,12 +1,14 @@
module Gitlab
module ImportExport
class VersionSaver
+ include Gitlab::ImportExport::CommandLineUtil
+
def initialize(shared:)
@shared = shared
end
def save
- FileUtils.mkdir_p(@shared.export_path)
+ mkdir_p(@shared.export_path)
File.write(version_file, Gitlab::ImportExport.version, mode: 'w')
rescue => e
diff --git a/lib/gitlab/import_export/wiki_repo_saver.rb b/lib/gitlab/import_export/wiki_repo_saver.rb
index 6107420e4dd..1e6722a7bba 100644
--- a/lib/gitlab/import_export/wiki_repo_saver.rb
+++ b/lib/gitlab/import_export/wiki_repo_saver.rb
@@ -9,7 +9,7 @@ module Gitlab
end
def bundle_to_disk(full_path)
- FileUtils.mkdir_p(@shared.export_path)
+ mkdir_p(@shared.export_path)
git_bundle(repo_path: path_to_repo, bundle_path: full_path)
rescue => e
@shared.error(e)
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index 3faab937726..c649da8c426 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -24,10 +24,20 @@ module Gitlab
end
def with
- @pool ||= ConnectionPool.new { ::Redis.new(params) }
+ @pool ||= ConnectionPool.new(size: pool_size) { ::Redis.new(params) }
@pool.with { |redis| yield redis }
end
+ def pool_size
+ if Sidekiq.server?
+ # the pool will be used in a multi-threaded context
+ Sidekiq.options[:concurrency] + 5
+ else
+ # probably this is a Unicorn process, so single threaded
+ 5
+ end
+ end
+
def _raw_config
return @_raw_config if defined?(@_raw_config)
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
index 2896636db5a..566658b508d 100644
--- a/spec/controllers/projects/boards/issues_controller_spec.rb
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe Projects::Boards::IssuesController do
let(:project) { create(:project_with_board) }
let(:user) { create(:user) }
+ let(:guest) { create(:user) }
let(:planning) { create(:label, project: project, name: 'Planning') }
let(:development) { create(:label, project: project, name: 'Development') }
@@ -12,6 +13,7 @@ describe Projects::Boards::IssuesController do
before do
project.team << [user, :master]
+ project.team << [guest, :guest]
end
describe 'GET index' do
@@ -61,6 +63,60 @@ describe Projects::Boards::IssuesController do
end
end
+ describe 'POST create' do
+ context 'with valid params' do
+ it 'returns a successful 200 response' do
+ create_issue user: user, list: list1, title: 'New issue'
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the created issue' do
+ create_issue user: user, list: list1, title: 'New issue'
+
+ expect(response).to match_response_schema('issue')
+ end
+ end
+
+ context 'with invalid params' do
+ context 'when title is nil' do
+ it 'returns an unprocessable entity 422 response' do
+ create_issue user: user, list: list1, title: nil
+
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'when list does not belongs to project board' do
+ it 'returns a not found 404 response' do
+ list = create(:list)
+
+ create_issue user: user, list: list, title: 'New issue'
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'with unauthorized user' do
+ it 'returns a forbidden 403 response' do
+ create_issue user: guest, list: list1, title: 'New issue'
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def create_issue(user:, list:, title:)
+ sign_in(user)
+
+ post :create, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ list_id: list.to_param,
+ issue: { title: title },
+ format: :json
+ end
+ end
+
describe 'PATCH update' do
let(:issue) { create(:labeled_issue, project: project, labels: [planning]) }
@@ -93,13 +149,7 @@ describe Projects::Boards::IssuesController do
end
context 'with unauthorized user' do
- let(:guest) { create(:user) }
-
- before do
- project.team << [guest, :guest]
- end
-
- it 'returns a successful 403 response' do
+ it 'returns a forbidden 403 response' do
move user: guest, issue: issue, from_list_id: list1.id, to_list_id: list2.id
expect(response).to have_http_status(403)
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 54a2d3d9460..19a8b1fe524 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -73,8 +73,8 @@ describe UsersController do
end
context 'forked project' do
- let!(:project) { create(:project) }
- let!(:forked_project) { Projects::ForkService.new(project, user).execute }
+ let(:project) { create(:project) }
+ let(:forked_project) { Projects::ForkService.new(project, user).execute }
before do
sign_in(user)
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
new file mode 100644
index 00000000000..c046e6b8d79
--- /dev/null
+++ b/spec/features/boards/new_issue_spec.rb
@@ -0,0 +1,80 @@
+require 'rails_helper'
+
+describe 'Issue Boards new issue', feature: true, js: true do
+ include WaitForAjax
+ include WaitForVueResource
+
+ let(:project) { create(:project_with_board, :public) }
+ let(:user) { create(:user) }
+
+ context 'authorized user' do
+ before do
+ project.team << [user, :master]
+
+ login_as(user)
+
+ visit namespace_project_board_path(project.namespace, project)
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 3)
+ end
+
+ it 'displays new issue button' do
+ expect(page).to have_selector('.board-issue-count-holder .btn', count: 1)
+ end
+
+ it 'does not display new issue button in done list' do
+ page.within('.board:nth-child(3)') do
+ expect(page).not_to have_selector('.board-issue-count-holder .btn')
+ end
+ end
+
+ it 'shows form when clicking button' do
+ page.within(first('.board')) do
+ find('.board-issue-count-holder .btn').click
+
+ expect(page).to have_selector('.board-new-issue-form')
+ end
+ end
+
+ it 'hides form when clicking cancel' do
+ page.within(first('.board')) do
+ find('.board-issue-count-holder .btn').click
+
+ expect(page).to have_selector('.board-new-issue-form')
+
+ click_button 'Cancel'
+
+ expect(page).to have_selector('.board-new-issue-form', visible: false)
+ end
+ end
+
+ it 'creates new issue' do
+ page.within(first('.board')) do
+ find('.board-issue-count-holder .btn').click
+ end
+
+ page.within(first('.board-new-issue-form')) do
+ find('.form-control').set('bug')
+ click_button 'Submit issue'
+ end
+
+ wait_for_vue_resource
+
+ page.within(first('.board .board-issue-count')) do
+ expect(page).to have_content('1')
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ before do
+ visit namespace_project_board_path(project.namespace, project)
+ wait_for_vue_resource
+ end
+
+ it 'does not display new issue button' do
+ expect(page).to have_selector('.board-issue-count-holder .btn', count: 0)
+ end
+ end
+end
diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb
index 5972e7f31c2..01a95bf49ac 100644
--- a/spec/features/projects/badges/coverage_spec.rb
+++ b/spec/features/projects/badges/coverage_spec.rb
@@ -59,7 +59,7 @@ feature 'test coverage badge' do
create(:ci_pipeline, opts).tap do |pipeline|
yield pipeline
- pipeline.build_updated
+ pipeline.update_status
end
end
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index 27c986c5187..52d08982c7a 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -47,6 +47,8 @@ feature 'Import/Export - project export integration test', feature: true, js: tr
expect(page).to have_content('Download export')
+ expect(file_permissions(project.export_path)).to eq(0700)
+
in_directory_with_expanded_export(project) do |exit_status, tmpdir|
expect(exit_status).to eq(0)
diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb
index b5a94fe0383..6498b7317b4 100644
--- a/spec/features/users_spec.rb
+++ b/spec/features/users_spec.rb
@@ -40,6 +40,17 @@ feature 'Users', feature: true do
expect(number_of_errors_on_page(page)).to be(1), 'errors on page:\n #{errors_on_page page}'
end
+ describe 'redirect alias routes' do
+ before { user }
+
+ scenario '/u/user1 redirects to user page' do
+ visit '/u/user1'
+
+ expect(current_path).to eq user_path(user)
+ expect(page).to have_text(user.name)
+ end
+ end
+
def errors_on_page(page)
page.find('#error_explanation').find('ul').all('li').map{ |item| item.text }.join("\n")
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 70032e7df94..8113742923b 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -11,7 +11,7 @@ describe ProjectsHelper do
describe "can_change_visibility_level?" do
let(:project) { create(:project) }
- let(:user) { create(:user) }
+ let(:user) { create(:project_member, :reporter, user: create(:user), project: project).user }
let(:fork_project) { Projects::ForkService.new(project, user).execute }
it "returns false if there are no appropriate permissions" do
@@ -72,7 +72,7 @@ describe ProjectsHelper do
it 'returns an HTML link to the user' do
link = helper.link_to_member(project, user)
- expect(link).to match(%r{/u/#{user.username}})
+ expect(link).to match(%r{/#{user.username}})
end
end
end
diff --git a/spec/lib/constraints/group_url_constrainer_spec.rb b/spec/lib/constraints/group_url_constrainer_spec.rb
new file mode 100644
index 00000000000..f0b75a664f2
--- /dev/null
+++ b/spec/lib/constraints/group_url_constrainer_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+
+describe GroupUrlConstrainer, lib: true do
+ let!(:username) { create(:group, path: 'gitlab-org') }
+
+ describe '#find_resource' do
+ it { expect(!!subject.find_resource('gitlab-org')).to be_truthy }
+ it { expect(!!subject.find_resource('gitlab-com')).to be_falsey }
+ end
+end
diff --git a/spec/lib/constraints/namespace_url_constrainer_spec.rb b/spec/lib/constraints/namespace_url_constrainer_spec.rb
new file mode 100644
index 00000000000..a5feaacb8ee
--- /dev/null
+++ b/spec/lib/constraints/namespace_url_constrainer_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe NamespaceUrlConstrainer, lib: true do
+ let!(:group) { create(:group, path: 'gitlab') }
+
+ describe '#matches?' do
+ context 'existing namespace' do
+ it { expect(subject.matches?(request '/gitlab')).to be_truthy }
+ it { expect(subject.matches?(request '/gitlab.atom')).to be_truthy }
+ it { expect(subject.matches?(request '/gitlab/')).to be_truthy }
+ it { expect(subject.matches?(request '//gitlab/')).to be_truthy }
+ end
+
+ context 'non-existing namespace' do
+ it { expect(subject.matches?(request '/gitlab-ce')).to be_falsey }
+ it { expect(subject.matches?(request '/gitlab.ce')).to be_falsey }
+ it { expect(subject.matches?(request '/g/gitlab')).to be_falsey }
+ it { expect(subject.matches?(request '/.gitlab')).to be_falsey }
+ end
+ end
+
+ def request(path)
+ OpenStruct.new(path: path)
+ end
+end
diff --git a/spec/lib/constraints/user_url_constrainer_spec.rb b/spec/lib/constraints/user_url_constrainer_spec.rb
new file mode 100644
index 00000000000..4b26692672f
--- /dev/null
+++ b/spec/lib/constraints/user_url_constrainer_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+
+describe UserUrlConstrainer, lib: true do
+ let!(:username) { create(:user, username: 'dz') }
+
+ describe '#find_resource' do
+ it { expect(!!subject.find_resource('dz')).to be_truthy }
+ it { expect(!!subject.find_resource('john')).to be_falsey }
+ end
+end
diff --git a/spec/lib/gitlab/badge/coverage/report_spec.rb b/spec/lib/gitlab/badge/coverage/report_spec.rb
index ab0cce6e091..1547bd3228c 100644
--- a/spec/lib/gitlab/badge/coverage/report_spec.rb
+++ b/spec/lib/gitlab/badge/coverage/report_spec.rb
@@ -100,7 +100,7 @@ describe Gitlab::Badge::Coverage::Report do
create(:ci_pipeline, opts).tap do |pipeline|
yield pipeline
- pipeline.build_updated
+ pipeline.update_status
end
end
end
diff --git a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
new file mode 100644
index 00000000000..b8e7932eb4a
--- /dev/null
+++ b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::AttributeCleaner, lib: true do
+ let(:unsafe_hash) do
+ {
+ 'service_id' => 99,
+ 'moved_to_id' => 99,
+ 'namespace_id' => 99,
+ 'ci_id' => 99,
+ 'random_project_id' => 99,
+ 'random_id' => 99,
+ 'milestone_id' => 99,
+ 'project_id' => 99,
+ 'user_id' => 99,
+ 'random_id_in_the_middle' => 99,
+ 'notid' => 99
+ }
+ end
+
+ let(:post_safe_hash) do
+ {
+ 'project_id' => 99,
+ 'user_id' => 99,
+ 'random_id_in_the_middle' => 99,
+ 'notid' => 99
+ }
+ end
+
+ it 'removes unwanted attributes from the hash' do
+ described_class.clean!(relation_hash: unsafe_hash)
+
+ expect(unsafe_hash).to eq(post_safe_hash)
+ end
+end
diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb
new file mode 100644
index 00000000000..3aa492a8ab1
--- /dev/null
+++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb
@@ -0,0 +1,125 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::RelationFactory, lib: true do
+ let(:project) { create(:empty_project) }
+ let(:members_mapper) { double('members_mapper').as_null_object }
+ let(:user) { create(:user) }
+ let(:created_object) do
+ described_class.create(relation_sym: relation_sym,
+ relation_hash: relation_hash,
+ members_mapper: members_mapper,
+ user: user,
+ project_id: project.id)
+ end
+
+ context 'hook object' do
+ let(:relation_sym) { :hooks }
+ let(:id) { 999 }
+ let(:service_id) { 99 }
+ let(:original_project_id) { 8 }
+ let(:token) { 'secret' }
+
+ let(:relation_hash) do
+ {
+ 'id' => id,
+ 'url' => 'https://example.json',
+ 'project_id' => original_project_id,
+ 'created_at' => '2016-08-12T09:41:03.462Z',
+ 'updated_at' => '2016-08-12T09:41:03.462Z',
+ 'service_id' => service_id,
+ 'push_events' => true,
+ 'issues_events' => false,
+ 'merge_requests_events' => true,
+ 'tag_push_events' => false,
+ 'note_events' => true,
+ 'enable_ssl_verification' => true,
+ 'build_events' => false,
+ 'wiki_page_events' => true,
+ 'token' => token
+ }
+ end
+
+ it 'does not have the original ID' do
+ expect(created_object.id).not_to eq(id)
+ end
+
+ it 'does not have the original service_id' do
+ expect(created_object.service_id).not_to eq(service_id)
+ end
+
+ it 'does not have the original project_id' do
+ expect(created_object.project_id).not_to eq(original_project_id)
+ end
+
+ it 'has the new project_id' do
+ expect(created_object.project_id).to eq(project.id)
+ end
+
+ it 'has a token' do
+ expect(created_object.token).to eq(token)
+ end
+
+ context 'original service exists' do
+ let(:service_id) { Service.create(project: project).id }
+
+ it 'does not have the original service_id' do
+ expect(created_object.service_id).not_to eq(service_id)
+ end
+ end
+ end
+
+ # Mocks an ActiveRecordish object with the dodgy columns
+ class FooModel
+ include ActiveModel::Model
+
+ def initialize(params)
+ params.each { |key, value| send("#{key}=", value) }
+ end
+
+ def values
+ instance_variables.map { |ivar| instance_variable_get(ivar) }
+ end
+ end
+
+ # `project_id`, `described_class.USER_REFERENCES`, noteable_id, target_id, and some project IDs are already
+ # re-assigned by described_class.
+ context 'Potentially hazardous foreign keys' do
+ let(:relation_sym) { :hazardous_foo_model }
+ let(:relation_hash) do
+ {
+ 'service_id' => 99,
+ 'moved_to_id' => 99,
+ 'namespace_id' => 99,
+ 'ci_id' => 99,
+ 'random_project_id' => 99,
+ 'random_id' => 99,
+ 'milestone_id' => 99,
+ 'project_id' => 99,
+ 'user_id' => 99,
+ }
+ end
+
+ class HazardousFooModel < FooModel
+ attr_accessor :service_id, :moved_to_id, :namespace_id, :ci_id, :random_project_id, :random_id, :milestone_id, :project_id
+ end
+
+ it 'does not preserve any foreign key IDs' do
+ expect(created_object.values).not_to include(99)
+ end
+ end
+
+ context 'Project references' do
+ let(:relation_sym) { :project_foo_model }
+ let(:relation_hash) do
+ Gitlab::ImportExport::RelationFactory::PROJECT_REFERENCES.map { |ref| { ref => 99 } }.inject(:merge)
+ end
+
+ class ProjectFooModel < FooModel
+ attr_accessor(*Gitlab::ImportExport::RelationFactory::PROJECT_REFERENCES)
+ end
+
+ it 'does not preserve any project foreign key IDs' do
+ expect(created_object.values).not_to include(99)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb
index cb54c020b31..74ff85e132a 100644
--- a/spec/lib/gitlab/redis_spec.rb
+++ b/spec/lib/gitlab/redis_spec.rb
@@ -88,6 +88,34 @@ describe Gitlab::Redis do
end
end
+ describe '.with' do
+ before { clear_pool }
+ after { clear_pool }
+
+ context 'when running not on sidekiq workers' do
+ before { allow(Sidekiq).to receive(:server?).and_return(false) }
+
+ it 'instantiates a connection pool with size 5' do
+ expect(ConnectionPool).to receive(:new).with(size: 5).and_call_original
+
+ described_class.with { |_redis| true }
+ end
+ end
+
+ context 'when running on sidekiq workers' do
+ before do
+ allow(Sidekiq).to receive(:server?).and_return(true)
+ allow(Sidekiq).to receive(:options).and_return({ concurrency: 18 })
+ end
+
+ it 'instantiates a connection pool with a size based on the concurrency of the worker' do
+ expect(ConnectionPool).to receive(:new).with(size: 18 + 5).and_call_original
+
+ described_class.with { |_redis| true }
+ end
+ end
+ end
+
describe '#raw_config_hash' do
it 'returns default redis url when no config file is present' do
expect(subject).to receive(:fetch_config) { false }
@@ -114,4 +142,10 @@ describe Gitlab::Redis do
rescue NameError
# raised if @_raw_config was not set; ignore
end
+
+ def clear_pool
+ described_class.remove_instance_variable(:@pool)
+ rescue NameError
+ # raised if @pool was not set; ignore
+ end
end
diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb
index 9c81d159cdf..1863581f57b 100644
--- a/spec/models/forked_project_link_spec.rb
+++ b/spec/models/forked_project_link_spec.rb
@@ -6,6 +6,7 @@ describe ForkedProjectLink, "add link on fork" do
let(:user) { create(:user, namespace: namespace) }
before do
+ create(:project_member, :reporter, user: user, project: project_from)
@project_to = fork_project(project_from, user)
end
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index cf713684463..26dd95bdfec 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -283,7 +283,7 @@ describe HipchatService, models: true do
context 'build events' do
let(:pipeline) { create(:ci_empty_pipeline) }
let(:build) { create(:ci_build, pipeline: pipeline) }
- let(:data) { Gitlab::DataBuilder::Build.build(build) }
+ let(:data) { Gitlab::DataBuilder::Build.build(build.reload) }
context 'for failed' do
before { build.drop }
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index e66faeed705..0f41f8dc7f1 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -10,7 +10,8 @@ describe API::Helpers, api: true do
let(:key) { create(:key, user: user) }
let(:params) { {} }
- let(:env) { {} }
+ let(:env) { { 'REQUEST_METHOD' => 'GET' } }
+ let(:request) { Rack::Request.new(env) }
def set_env(token_usr, identifier)
clear_env
@@ -52,17 +53,43 @@ describe API::Helpers, api: true do
describe ".current_user" do
subject { current_user }
- describe "when authenticating via Warden" do
+ describe "Warden authentication" do
before { doorkeeper_guard_returns false }
- context "fails" do
- it { is_expected.to be_nil }
+ context "with invalid credentials" do
+ context "GET request" do
+ before { env['REQUEST_METHOD'] = 'GET' }
+ it { is_expected.to be_nil }
+ end
end
- context "succeeds" do
+ context "with valid credentials" do
before { warden_authenticate_returns user }
- it { is_expected.to eq(user) }
+ context "GET request" do
+ before { env['REQUEST_METHOD'] = 'GET' }
+ it { is_expected.to eq(user) }
+ end
+
+ context "HEAD request" do
+ before { env['REQUEST_METHOD'] = 'HEAD' }
+ it { is_expected.to eq(user) }
+ end
+
+ context "PUT request" do
+ before { env['REQUEST_METHOD'] = 'PUT' }
+ it { is_expected.to be_nil }
+ end
+
+ context "POST request" do
+ before { env['REQUEST_METHOD'] = 'POST' }
+ it { is_expected.to be_nil }
+ end
+
+ context "DELETE request" do
+ before { env['REQUEST_METHOD'] = 'DELETE' }
+ it { is_expected.to be_nil }
+ end
end
end
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
new file mode 100644
index 00000000000..f4b04445c6c
--- /dev/null
+++ b/spec/requests/api/boards_spec.rb
@@ -0,0 +1,192 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:guest) { create(:user) }
+ let(:admin) { create(:user, :admin) }
+ let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
+
+ let!(:dev_label) do
+ create(:label, title: 'Development', color: '#FFAABB', project: project)
+ end
+
+ let!(:test_label) do
+ create(:label, title: 'Testing', color: '#FFAACC', project: project)
+ end
+
+ let!(:ux_label) do
+ create(:label, title: 'UX', color: '#FF0000', project: project)
+ end
+
+ let!(:dev_list) do
+ create(:list, label: dev_label, position: 1)
+ end
+
+ let!(:test_list) do
+ create(:list, label: test_label, position: 2)
+ end
+
+ let!(:board) do
+ create(:board, project: project, lists: [dev_list, test_list])
+ end
+
+ before do
+ project.team << [user, :reporter]
+ project.team << [guest, :guest]
+ end
+
+ describe "GET /projects/:id/boards" do
+ let(:base_url) { "/projects/#{project.id}/boards" }
+
+ context "when unauthenticated" do
+ it "returns authentication error" do
+ get api(base_url)
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when authenticated" do
+ it "returns the project issue board" do
+ get api(base_url, user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(board.id)
+ expect(json_response.first['lists']).to be_an Array
+ expect(json_response.first['lists'].length).to eq(2)
+ expect(json_response.first['lists'].last).to have_key('position')
+ end
+ end
+ end
+
+ describe "GET /projects/:id/boards/:board_id/lists" do
+ let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+ it 'returns issue board lists' do
+ get api(base_url, user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['label']['name']).to eq(dev_label.title)
+ end
+
+ it 'returns 404 if board not found' do
+ get api("/projects/#{project.id}/boards/22343/lists", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe "GET /projects/:id/boards/:board_id/lists/:list_id" do
+ let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+ it 'returns a list' do
+ get api("#{base_url}/#{dev_list.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(dev_list.id)
+ expect(json_response['label']['name']).to eq(dev_label.title)
+ expect(json_response['position']).to eq(1)
+ end
+
+ it 'returns 404 if list not found' do
+ get api("#{base_url}/5324", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe "POST /projects/:id/board/lists" do
+ let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+ it 'creates a new issue board list' do
+ post api(base_url, user),
+ label_id: ux_label.id
+
+ expect(response).to have_http_status(201)
+ expect(json_response['label']['name']).to eq(ux_label.title)
+ expect(json_response['position']).to eq(3)
+ end
+
+ it 'returns 400 when creating a new list if label_id is invalid' do
+ post api(base_url, user),
+ label_id: 23423
+
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 403 for project members with guest role" do
+ put api("#{base_url}/#{test_list.id}", guest),
+ position: 1
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe "PUT /projects/:id/boards/:board_id/lists/:list_id to update only position" do
+ let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+ it "updates a list" do
+ put api("#{base_url}/#{test_list.id}", user),
+ position: 1
+
+ expect(response).to have_http_status(200)
+ expect(json_response['position']).to eq(1)
+ end
+
+ it "returns 404 error if list id not found" do
+ put api("#{base_url}/44444", user),
+ position: 1
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns 403 for project members with guest role" do
+ put api("#{base_url}/#{test_list.id}", guest),
+ position: 1
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe "DELETE /projects/:id/board/lists/:list_id" do
+ let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+ it "rejects a non member from deleting a list" do
+ delete api("#{base_url}/#{dev_list.id}", non_member)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "rejects a user with guest role from deleting a list" do
+ delete api("#{base_url}/#{dev_list.id}", guest)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "returns 404 error if list id not found" do
+ delete api("#{base_url}/44444", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ context "when the user is project owner" do
+ let(:owner) { create(:user) }
+ let(:project) { create(:project, namespace: owner.namespace) }
+
+ it "deletes the list if an admin requests it" do
+ delete api("#{base_url}/#{dev_list.id}", owner)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['position']).to eq(1)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 10f772c5b1a..aa610557056 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -5,7 +5,7 @@ describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:user2) { create(:user) }
- let!(:project) { create(:project, creator_id: user.id) }
+ let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') }
@@ -13,7 +13,7 @@ describe API::API, api: true do
before { project.team << [user, :reporter] }
- describe "GET /projects/:id/repository/commits" do
+ describe "List repository commits" do
context "authorized user" do
before { project.team << [user2, :reporter] }
@@ -69,7 +69,268 @@ describe API::API, api: true do
end
end
- describe "GET /projects:id/repository/commits/:sha" do
+ describe "Create a commit with multiple files and actions" do
+ let!(:url) { "/projects/#{project.id}/repository/commits" }
+
+ it 'returns a 403 unauthorized for user without permissions' do
+ post api(url, user2)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it 'returns a 400 bad request if no params are given' do
+ post api(url, user)
+
+ expect(response).to have_http_status(400)
+ end
+
+ context :create do
+ let(:message) { 'Created file' }
+ let!(:invalid_c_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'create',
+ file_path: 'files/ruby/popen.rb',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+ let!(:valid_c_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'create',
+ file_path: 'foo/bar/baz.txt',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+
+ it 'a new file in project repo' do
+ post api(url, user), valid_c_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'returns a 400 bad request if file exists' do
+ post api(url, user), invalid_c_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context :delete do
+ let(:message) { 'Deleted file' }
+ let!(:invalid_d_params) do
+ {
+ branch_name: 'markdown',
+ commit_message: message,
+ actions: [
+ {
+ action: 'delete',
+ file_path: 'doc/api/projects.md'
+ }
+ ]
+ }
+ end
+ let!(:valid_d_params) do
+ {
+ branch_name: 'markdown',
+ commit_message: message,
+ actions: [
+ {
+ action: 'delete',
+ file_path: 'doc/api/users.md'
+ }
+ ]
+ }
+ end
+
+ it 'an existing file in project repo' do
+ post api(url, user), valid_d_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'returns a 400 bad request if file does not exist' do
+ post api(url, user), invalid_d_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context :move do
+ let(:message) { 'Moved file' }
+ let!(:invalid_m_params) do
+ {
+ branch_name: 'feature',
+ commit_message: message,
+ actions: [
+ {
+ action: 'move',
+ file_path: 'CHANGELOG',
+ previous_path: 'VERSION',
+ content: '6.7.0.pre'
+ }
+ ]
+ }
+ end
+ let!(:valid_m_params) do
+ {
+ branch_name: 'feature',
+ commit_message: message,
+ actions: [
+ {
+ action: 'move',
+ file_path: 'VERSION.txt',
+ previous_path: 'VERSION',
+ content: '6.7.0.pre'
+ }
+ ]
+ }
+ end
+
+ it 'an existing file in project repo' do
+ post api(url, user), valid_m_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'returns a 400 bad request if file does not exist' do
+ post api(url, user), invalid_m_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context :update do
+ let(:message) { 'Updated file' }
+ let!(:invalid_u_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'update',
+ file_path: 'foo/bar.baz',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+ let!(:valid_u_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'update',
+ file_path: 'files/ruby/popen.rb',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+
+ it 'an existing file in project repo' do
+ post api(url, user), valid_u_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'returns a 400 bad request if file does not exist' do
+ post api(url, user), invalid_u_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context "multiple operations" do
+ let(:message) { 'Multiple actions' }
+ let!(:invalid_mo_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'create',
+ file_path: 'files/ruby/popen.rb',
+ content: 'puts 8'
+ },
+ {
+ action: 'delete',
+ file_path: 'doc/api/projects.md'
+ },
+ {
+ action: 'move',
+ file_path: 'CHANGELOG',
+ previous_path: 'VERSION',
+ content: '6.7.0.pre'
+ },
+ {
+ action: 'update',
+ file_path: 'foo/bar.baz',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+ let!(:valid_mo_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'create',
+ file_path: 'foo/bar/baz.txt',
+ content: 'puts 8'
+ },
+ {
+ action: 'delete',
+ file_path: 'Gemfile.zip'
+ },
+ {
+ action: 'move',
+ file_path: 'VERSION.txt',
+ previous_path: 'VERSION',
+ content: '6.7.0.pre'
+ },
+ {
+ action: 'update',
+ file_path: 'files/ruby/popen.rb',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+
+ it 'are commited as one in project repo' do
+ post api(url, user), valid_mo_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'return a 400 bad request if there are any issues' do
+ post api(url, user), invalid_mo_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+ end
+
+ describe "Get a single commit" do
context "authorized user" do
it "returns a commit by sha" do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
@@ -122,7 +383,7 @@ describe API::API, api: true do
end
end
- describe "GET /projects:id/repository/commits/:sha/diff" do
+ describe "Get the diff of a commit" do
context "authorized user" do
before { project.team << [user2, :reporter] }
@@ -149,7 +410,7 @@ describe API::API, api: true do
end
end
- describe 'GET /projects:id/repository/commits/:sha/comments' do
+ describe 'Get the comments of a commit' do
context 'authorized user' do
it 'returns merge_request comments' do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
@@ -174,7 +435,7 @@ describe API::API, api: true do
end
end
- describe 'POST /projects:id/repository/commits/:sha/comments' do
+ describe 'Post comment to commit' do
context 'authorized user' do
it 'returns comment' do
post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment'
diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb
index 34f84f78952..e38d5745d44 100644
--- a/spec/requests/api/fork_spec.rb
+++ b/spec/requests/api/fork_spec.rb
@@ -18,7 +18,7 @@ describe API::API, api: true do
end
let(:project_user2) do
- create(:project_member, :guest, user: user2, project: project)
+ create(:project_member, :reporter, user: user2, project: project)
end
describe 'POST /projects/fork/:id' do
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 92032f09b17..d22e0595788 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -97,7 +97,10 @@ describe API::Members, api: true do
shared_examples 'POST /:sources/:id/members' do |source_type|
context "with :sources == #{source_type.pluralize}" do
it_behaves_like 'a 404 response when source is private' do
- let(:route) { post api("/#{source_type.pluralize}/#{source.id}/members", stranger) }
+ let(:route) do
+ post api("/#{source_type.pluralize}/#{source.id}/members", stranger),
+ user_id: access_requester.id, access_level: Member::MASTER
+ end
end
context 'when authenticated as a non-member or member with insufficient rights' do
@@ -105,7 +108,8 @@ describe API::Members, api: true do
context "as a #{type}" do
it 'returns 403' do
user = public_send(type)
- post api("/#{source_type.pluralize}/#{source.id}/members", user)
+ post api("/#{source_type.pluralize}/#{source.id}/members", user),
+ user_id: access_requester.id, access_level: Member::MASTER
expect(response).to have_http_status(403)
end
@@ -174,7 +178,10 @@ describe API::Members, api: true do
shared_examples 'PUT /:sources/:id/members/:user_id' do |source_type|
context "with :sources == #{source_type.pluralize}" do
it_behaves_like 'a 404 response when source is private' do
- let(:route) { put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
+ let(:route) do
+ put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger),
+ access_level: Member::MASTER
+ end
end
context 'when authenticated as a non-member or member with insufficient rights' do
@@ -182,7 +189,8 @@ describe API::Members, api: true do
context "as a #{type}" do
it 'returns 403' do
user = public_send(type)
- put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+ put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user),
+ access_level: Member::MASTER
expect(response).to have_http_status(403)
end
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 4bc3cddd9c2..0dd00af878d 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -9,7 +9,9 @@ require 'spec_helper'
# user_calendar_activities GET /u/:username/calendar_activities(.:format)
describe UsersController, "routing" do
it "to #show" do
- expect(get("/u/User")).to route_to('users#show', username: 'User')
+ allow(User).to receive(:find_by).and_return(true)
+
+ expect(get("/User")).to route_to('users#show', username: 'User')
end
it "to #groups" do
diff --git a/spec/services/boards/issues/create_service_spec.rb b/spec/services/boards/issues/create_service_spec.rb
new file mode 100644
index 00000000000..33e10e79f6d
--- /dev/null
+++ b/spec/services/boards/issues/create_service_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Boards::Issues::CreateService, services: true do
+ describe '#execute' do
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+ let(:user) { create(:user) }
+ let(:label) { create(:label, project: project, name: 'in-progress') }
+ let!(:list) { create(:list, board: board, label: label, position: 0) }
+
+ subject(:service) { described_class.new(project, user, title: 'New issue') }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'delegates the create proceedings to Issues::CreateService' do
+ expect_any_instance_of(Issues::CreateService).to receive(:execute).once
+
+ service.execute(list)
+ end
+
+ it 'creates a new issue' do
+ expect { service.execute(list) }.to change(project.issues, :count).by(1)
+ end
+
+ it 'adds the label of the list to the issue' do
+ issue = service.execute(list)
+
+ expect(issue.labels).to eq [label]
+ end
+ end
+end
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index e65da15aca8..5b9f454fd2d 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -30,7 +30,7 @@ describe Boards::Issues::ListService, services: true do
let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3]) }
let!(:closed_issue3) { create(:issue, :closed, project: project) }
- let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1, development]) }
+ let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1]) }
before do
project.team << [user, :developer]
@@ -58,15 +58,15 @@ describe Boards::Issues::ListService, services: true do
issues = described_class.new(project, user, params).execute
- expect(issues).to eq [closed_issue2, closed_issue3, closed_issue1]
+ expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1]
end
- it 'returns opened/closed issues that have label list applied when listing issues from a label list' do
+ it 'returns opened issues that have label list applied when listing issues from a label list' do
params = { id: list1.id }
issues = described_class.new(project, user, params).execute
- expect(issues).to eq [closed_issue4, list1_issue3, list1_issue1, list1_issue2]
+ expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2]
end
end
end
diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb
index d019e50649f..d3c37c7820f 100644
--- a/spec/services/files/update_service_spec.rb
+++ b/spec/services/files/update_service_spec.rb
@@ -41,7 +41,7 @@ describe Files::UpdateService do
it "returns a hash with the :success status " do
results = subject.execute
- expect(results).to match({ status: :success })
+ expect(results[:status]).to match(:success)
end
it "updates the file with the new contents" do
@@ -69,7 +69,7 @@ describe Files::UpdateService do
it "returns a hash with the :success status " do
results = subject.execute
- expect(results).to match({ status: :success })
+ expect(results[:status]).to match(:success)
end
it "updates the file with the new contents" do
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index ef2036c78b1..64d15c0523c 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -12,12 +12,26 @@ describe Projects::ForkService, services: true do
description: 'wow such project')
@to_namespace = create(:namespace)
@to_user = create(:user, namespace: @to_namespace)
+ @from_project.add_user(@to_user, :developer)
end
context 'fork project' do
+ context 'when forker is a guest' do
+ before do
+ @guest = create(:user)
+ @from_project.add_user(@guest, :guest)
+ end
+ subject { fork_project(@from_project, @guest) }
+
+ it { is_expected.not_to be_persisted }
+ it { expect(subject.errors[:forked_from_project_id]).to eq(['is forbidden']) }
+ end
+
describe "successfully creates project in the user namespace" do
let(:to_project) { fork_project(@from_project, @to_user) }
+ it { expect(to_project).to be_persisted }
+ it { expect(to_project.errors).to be_empty }
it { expect(to_project.owner).to eq(@to_user) }
it { expect(to_project.namespace).to eq(@to_user.namespace) }
it { expect(to_project.star_count).to be_zero }
@@ -29,7 +43,9 @@ describe Projects::ForkService, services: true do
it "fails due to validation, not transaction failure" do
@existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
@to_project = fork_project(@from_project, @to_user)
- expect(@existing_project.persisted?).to be_truthy
+ expect(@existing_project).to be_persisted
+
+ expect(@to_project).not_to be_persisted
expect(@to_project.errors[:name]).to eq(['has already been taken'])
expect(@to_project.errors[:path]).to eq(['has already been taken'])
end
@@ -81,18 +97,23 @@ describe Projects::ForkService, services: true do
@group = create(:group)
@group.add_user(@group_owner, GroupMember::OWNER)
@group.add_user(@developer, GroupMember::DEVELOPER)
+ @project.add_user(@developer, :developer)
+ @project.add_user(@group_owner, :developer)
@opts = { namespace: @group }
end
context 'fork project for group' do
it 'group owner successfully forks project into the group' do
to_project = fork_project(@project, @group_owner, @opts)
+
+ expect(to_project).to be_persisted
+ expect(to_project.errors).to be_empty
expect(to_project.owner).to eq(@group)
expect(to_project.namespace).to eq(@group)
expect(to_project.name).to eq(@project.name)
expect(to_project.path).to eq(@project.path)
expect(to_project.description).to eq(@project.description)
- expect(to_project.star_count).to be_zero
+ expect(to_project.star_count).to be_zero
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index b16840a1238..d1a47ea9b6f 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -451,7 +451,7 @@ describe SystemNoteService, services: true do
end
context 'commit with cross-reference from fork' do
- let(:author2) { create(:user) }
+ let(:author2) { create(:project_member, :reporter, user: create(:user), project: project).user }
let(:forked_project) { Projects::ForkService.new(project, author2).execute }
let(:commit2) { forked_project.commit }
@@ -561,7 +561,7 @@ describe SystemNoteService, services: true do
describe "existing reference" do
before do
- message = %Q{[#{author.name}|http://localhost/u/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\\n'#{commit.title}'}
+ message = %Q{[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\\n'#{commit.title}'}
WebMock.stub_request(:get, jira_api_comment_url).to_return(body: %Q({"comments":[{"body":"#{message}"}]}))
end
diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb
index be0772d6a4a..1b0a4583f5c 100644
--- a/spec/support/import_export/export_file_helper.rb
+++ b/spec/support/import_export/export_file_helper.rb
@@ -130,4 +130,8 @@ module ExportFileHelper
(parsed_model_attributes - parent.keys - excluded_attributes).empty?
end
+
+ def file_permissions(file)
+ File.stat(file).mode & 0777
+ end
end
diff --git a/spec/views/ci/lints/show.html.haml_spec.rb b/spec/views/ci/lints/show.html.haml_spec.rb
index 793b747e7eb..2dac5ee23c8 100644
--- a/spec/views/ci/lints/show.html.haml_spec.rb
+++ b/spec/views/ci/lints/show.html.haml_spec.rb
@@ -1,6 +1,52 @@
require 'spec_helper'
describe 'ci/lints/show' do
+ include Devise::TestHelpers
+
+ describe 'XSS protection' do
+ let(:config_processor) { Ci::GitlabCiYamlProcessor.new(YAML.dump(content)) }
+ before do
+ assign(:status, true)
+ assign(:builds, config_processor.builds)
+ assign(:stages, config_processor.stages)
+ assign(:jobs, config_processor.jobs)
+ end
+
+ context 'when builds attrbiutes contain HTML nodes' do
+ let(:content) do
+ {
+ rspec: {
+ script: '<h1>rspec</h1>',
+ stage: 'test'
+ }
+ }
+ end
+
+ it 'does not render HTML elements' do
+ render
+
+ expect(rendered).not_to have_css('h1', text: 'rspec')
+ end
+ end
+
+ context 'when builds attributes do not contain HTML nodes' do
+ let(:content) do
+ {
+ rspec: {
+ script: 'rspec',
+ stage: 'test'
+ }
+ }
+ end
+
+ it 'shows configuration in the table' do
+ render
+
+ expect(rendered).to have_css('td pre', text: 'rspec')
+ end
+ end
+ end
+
let(:content) do
{
build_template: {
diff --git a/spec/workers/process_pipeline_worker_spec.rb b/spec/workers/process_pipeline_worker_spec.rb
new file mode 100644
index 00000000000..7b5f98d5763
--- /dev/null
+++ b/spec/workers/process_pipeline_worker_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe ProcessPipelineWorker do
+ describe '#perform' do
+ context 'when pipeline exists' do
+ let(:pipeline) { create(:ci_pipeline) }
+
+ it 'processes pipeline' do
+ expect_any_instance_of(Ci::Pipeline).to receive(:process!)
+
+ described_class.new.perform(pipeline.id)
+ end
+ end
+
+ context 'when pipeline does not exist' do
+ it 'does not raise exception' do
+ expect { described_class.new.perform(123) }
+ .not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/workers/update_pipeline_worker_spec.rb b/spec/workers/update_pipeline_worker_spec.rb
new file mode 100644
index 00000000000..fadc42b22f0
--- /dev/null
+++ b/spec/workers/update_pipeline_worker_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe UpdatePipelineWorker do
+ describe '#perform' do
+ context 'when pipeline exists' do
+ let(:pipeline) { create(:ci_pipeline) }
+
+ it 'updates pipeline status' do
+ expect_any_instance_of(Ci::Pipeline).to receive(:update_status)
+
+ described_class.new.perform(pipeline.id)
+ end
+ end
+
+ context 'when pipeline does not exist' do
+ it 'does not raise exception' do
+ expect { described_class.new.perform(123) }
+ .not_to raise_error
+ end
+ end
+ end
+end