summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintignore1
-rw-r--r--CHANGELOG.md24
-rw-r--r--CONTRIBUTING.md17
-rw-r--r--Gemfile4
-rw-r--r--Gemfile.lock8
-rw-r--r--app/assets/javascripts/application.js3
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js.es62
-rw-r--r--app/assets/javascripts/graphs/graphs_bundle.js2
-rw-r--r--app/assets/javascripts/merge_request_tabs.js18
-rw-r--r--app/assets/javascripts/network/network_bundle.js2
-rw-r--r--app/assets/javascripts/single_file_diff.js19
-rw-r--r--app/assets/javascripts/star.js2
-rw-r--r--app/assets/stylesheets/framework/avatar.scss63
-rw-r--r--app/assets/stylesheets/framework/buttons.scss2
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss87
-rw-r--r--app/assets/stylesheets/framework/header.scss6
-rw-r--r--app/assets/stylesheets/framework/lists.scss4
-rw-r--r--app/assets/stylesheets/framework/pagination.scss62
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss5
-rw-r--r--app/assets/stylesheets/pages/awards.scss8
-rw-r--r--app/assets/stylesheets/pages/boards.scss5
-rw-r--r--app/assets/stylesheets/pages/builds.scss26
-rw-r--r--app/assets/stylesheets/pages/dashboard.scss4
-rw-r--r--app/assets/stylesheets/pages/groups.scss9
-rw-r--r--app/assets/stylesheets/pages/notes.scss5
-rw-r--r--app/assets/stylesheets/pages/projects.scss4
-rw-r--r--app/assets/stylesheets/pages/todos.scss60
-rw-r--r--app/controllers/admin/users_controller.rb2
-rw-r--r--app/controllers/projects/labels_controller.rb2
-rw-r--r--app/controllers/projects/project_members_controller.rb17
-rw-r--r--app/controllers/projects_controller.rb3
-rw-r--r--app/finders/issuable_finder.rb4
-rw-r--r--app/helpers/events_helper.rb6
-rw-r--r--app/models/concerns/project_features_compatibility.rb2
-rw-r--r--app/models/group_label.rb4
-rw-r--r--app/models/label.rb30
-rw-r--r--app/models/project.rb7
-rw-r--r--app/models/project_label.rb4
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/models/user.rb18
-rw-r--r--app/policies/project_policy.rb12
-rw-r--r--app/services/members/create_service.rb16
-rw-r--r--app/services/merge_requests/build_service.rb50
-rw-r--r--app/views/admin/groups/_group.html.haml3
-rw-r--r--app/views/admin/groups/show.html.haml3
-rw-r--r--app/views/admin/projects/index.html.haml3
-rw-r--r--app/views/admin/users/index.html.haml2
-rw-r--r--app/views/dashboard/todos/index.html.haml147
-rw-r--r--app/views/groups/edit.html.haml3
-rw-r--r--app/views/groups/labels/index.html.haml2
-rw-r--r--app/views/groups/show.html.haml3
-rw-r--r--app/views/kaminari/gitlab/_gap.html.haml4
-rw-r--r--app/views/kaminari/gitlab/_page.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml6
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml85
-rw-r--r--app/views/projects/_home_panel.html.haml3
-rw-r--r--app/views/projects/boards/components/_board.html.haml4
-rw-r--r--app/views/projects/builds/_header.html.haml29
-rw-r--r--app/views/projects/builds/_sidebar.html.haml2
-rw-r--r--app/views/projects/buttons/_fork.html.haml4
-rw-r--r--app/views/projects/buttons/_star.html.haml2
-rw-r--r--app/views/projects/edit.html.haml4
-rw-r--r--app/views/projects/labels/index.html.haml4
-rw-r--r--app/views/projects/wikis/_form.html.haml9
-rw-r--r--app/views/projects/wikis/_main_links.html.haml3
-rw-r--r--app/views/projects/wikis/edit.html.haml2
-rw-r--r--app/views/shared/_event_filter.html.haml9
-rw-r--r--app/views/shared/_label.html.haml13
-rw-r--r--app/views/shared/empty_states/_todos_all_done.svg1
-rw-r--r--app/views/shared/empty_states/_todos_empty.svg110
-rw-r--r--app/views/shared/groups/_group.html.haml3
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml4
-rw-r--r--app/views/shared/projects/_project.html.haml9
-rw-r--r--app/views/users/_groups.html.haml3
-rw-r--r--app/workers/post_receive.rb6
-rwxr-xr-xbin/changelog164
-rw-r--r--config/dependency_decisions.yml7
-rw-r--r--config/gitlab.yml.example4
-rw-r--r--config/initializers/0_post_deployment_migrations.rb12
-rw-r--r--config/initializers/sidekiq.rb16
-rw-r--r--db/post_migrate/.gitkeep0
-rw-r--r--doc/administration/raketasks/maintenance.md220
-rw-r--r--doc/administration/troubleshooting/debug.md2
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/projects.md8
-rw-r--r--doc/api/tags.md2
-rw-r--r--doc/development/README.md2
-rw-r--r--doc/development/changelog.md164
-rw-r--r--doc/development/gotchas.md6
-rw-r--r--doc/development/licensing.md3
-rw-r--r--doc/development/post_deployment_migrations.md75
-rw-r--r--doc/development/testing.md40
-rw-r--r--doc/integration/jira.md4
-rw-r--r--doc/integration/saml.md21
-rw-r--r--doc/raketasks/maintenance.md189
-rw-r--r--doc/update/README.md2
-rw-r--r--doc/update/upgrading_postgresql_using_slony.md482
-rw-r--r--features/dashboard/active_tab.feature6
-rw-r--r--features/dashboard/dashboard.feature1
-rw-r--r--features/steps/dashboard/help.rb2
-rw-r--r--features/steps/shared/sidebar_active_tab.rb14
-rw-r--r--generator_templates/rails/post_deployment_migration/migration.rb22
-rw-r--r--lib/api/helpers.rb10
-rw-r--r--lib/api/project_hooks.rb6
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb20
-rw-r--r--lib/banzai/filter/external_issue_reference_filter.rb11
-rw-r--r--lib/banzai/filter/reference_filter.rb6
-rw-r--r--lib/banzai/filter/user_reference_filter.rb41
-rw-r--r--lib/banzai/redactor.rb8
-rw-r--r--lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb15
-rw-r--r--lib/gitlab/utils.rb8
-rw-r--r--lib/tasks/gitlab/shell.rake15
-rw-r--r--spec/bin/changelog_spec.rb65
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb68
-rw-r--r--spec/features/boards/boards_spec.rb8
-rw-r--r--spec/features/issues/filter_by_milestone_spec.rb6
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb8
-rw-r--r--spec/features/projects/builds_spec.rb4
-rw-r--r--spec/features/projects/features_visibility_spec.rb18
-rw-r--r--spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb44
-rw-r--r--spec/features/todos/todos_spec.rb8
-rw-r--r--spec/javascripts/spec_helper.js2
-rw-r--r--spec/lib/banzai/filter/external_issue_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb38
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb6
-rw-r--r--spec/lib/banzai/pipeline/full_pipeline_spec.rb28
-rw-r--r--spec/lib/banzai/redactor_spec.rb75
-rw-r--r--spec/lib/gitlab/git_access_spec.rb8
-rw-r--r--spec/lib/gitlab/utils_spec.rb35
-rw-r--r--spec/models/concerns/project_features_compatibility_spec.rb14
-rw-r--r--spec/models/project_services/jira_service_spec.rb22
-rw-r--r--spec/models/project_spec.rb6
-rw-r--r--spec/models/user_spec.rb74
-rw-r--r--spec/policies/project_policy_spec.rb14
-rw-r--r--spec/rake_helper.rb19
-rw-r--r--spec/requests/api/api_helpers_spec.rb30
-rw-r--r--spec/requests/api/project_hooks_spec.rb32
-rw-r--r--spec/requests/lfs_http_spec.rb12
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb28
-rw-r--r--spec/services/members/create_service_spec.rb25
-rw-r--r--spec/services/merge_requests/build_service_spec.rb49
-rw-r--r--spec/services/projects/create_service_spec.rb2
-rw-r--r--spec/support/banzai/reference_filter_shared_examples.rb13
-rw-r--r--spec/support/rake_helpers.rb10
-rw-r--r--spec/tasks/gitlab/shell_rake_spec.rb26
145 files changed, 2791 insertions, 735 deletions
diff --git a/.eslintignore b/.eslintignore
index 453747e14e1..a57137b4d70 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,3 +1,4 @@
+/coverage-javascript/
/public/
/tmp/
/vendor/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9491be9cd1e..36ec61dd46f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,26 +1,36 @@
Please view this file on the master branch, on stable branches it's out of date.
## 8.14.0 (2016-11-22)
-
+- Fix Milestone dropdown not stay selected for `Upcoming` and `No Milestone` option !7117
- Backups do not fail anymore when using tar on annex and custom_hooks only. !5814
- Adds user project membership expired event to clarify why user was removed (Callum Dryden)
- Trim leading and trailing whitespace on project_path (Linus Thiel)
- Prevent award emoji via notes for issues/MRs authored by user (barthc)
+- Adds support for the `token` attribute in project hooks API (Gauvain Pocentek)
- Adds an optional path parameter to the Commits API to filter commits by path (Luis HGO)
+- Fix Markdown styling inside reference links (Jan Zdráhal)
- Fix extra space on Build sidebar on Firefox !7060
+- Fail gracefully when creating merge request with non-existing branch (alexsanford)
- Fix mobile layout issues in admin user overview page !7087
- Fix HipChat notifications rendering (airatshigapov, eisnerd)
+- Remove 'Edit' button from wiki edit view !7143 (Hiroyuki Sato)
- Refactor Jira service to use jira-ruby gem
+- Improved todos empty state
- Add hover to trash icon in notes !7008 (blackst0ne)
+- Hides project activity tabs when features are disabled
- Only show one error message for an invalid email !5905 (lycoperdon)
+- Added guide describing how to upgrade PostgreSQL using Slony
- Fix sidekiq stats in admin area (blackst0ne)
+- Added label description as tooltip to issue board list title
- Created cycle analytics bundle JavaScript file
- API: Fix booleans not recognized as such when using the `to_boolean` helper
- Removed delete branch tooltip !6954
- Stop unauthorized users dragging on milestone page (blackst0ne)
- Restore issue boards welcome message when a project is created !6899
+- Do not show tooltip for active element !7105 (winniehell)
- Escape ref and path for relative links !6050 (winniehell)
- Fixed link typo on /help/ui to Alerts section. !6915 (Sam Rose)
+- Fix broken issue/merge request links in JIRA comments. !6143 (Brian Kintz)
- Fix filtering of milestones with quotes in title (airatshigapov)
- Fix issue boards dragging bug in Safari
- Refactor less readable existance checking code from CoffeeScript !6289 (jlogandavison)
@@ -31,16 +41,28 @@ Please view this file on the master branch, on stable branches it's out of date.
- New issue board list dropdown stays open after adding a new list
- Fix: Backup restore doesn't clear cache
- Optimize Event queries by removing default order
+- Remove duplicate links from sidebar
- API: Fix project deploy keys 400 and 500 errors when adding an existing key. !6784 (Joshua Welsh)
+- Add Rake task to create/repair GitLab Shell hooks symlinks !5634
- Add job for removal of unreferenced LFS objects from both the database and the filesystem (Frank Groeneveld)
- Replace jquery.cookie plugin with js.cookie !7085
- Use MergeRequestsClosingIssues cache data on Issue#closed_by_merge_requests method
- Fix Sign in page 'Forgot your password?' link overlaps on medium-large screens
- Show full status link on MR & commit pipelines
- Fix documents and comments on Build API `scope`
+- Initialize Sidekiq with the list of queues used by GitLab
- Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov)
- Shortened merge request modal to let clipboard button not overlap
- In all filterable drop downs, put input field in focus only after load is complete (Ido @leibo)
+- Improve search query parameter naming in /admin/users !7115 (YarNayar)
+- Fix table pagination to be responsive
+- Allow to search for user by secondary email address in the admin interface(/admin/users) !7115 (YarNayar)
+
+## 8.13.3
+
+- Fix relative links in Markdown wiki when displayed in "Project" tab !7218
+- Reduce the overhead to calculate number of open/closed issues and merge requests within the group or project
+- Fix project features default values
## 8.13.2 (2016-10-31)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b4635e50c28..67c30c2424c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -19,7 +19,6 @@
- [Technical debt](#technical-debt)
- [Merge requests](#merge-requests)
- [Merge request guidelines](#merge-request-guidelines)
- - [Merge request description format](#merge-request-description-format)
- [Contribution acceptance criteria](#contribution-acceptance-criteria)
- [Changes for Stable Releases](#changes-for-stable-releases)
- [Definition of done](#definition-of-done)
@@ -247,13 +246,7 @@ request is as follows:
1. Fork the project into your personal space on GitLab.com
1. Create a feature branch, branch away from `master`
1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code
-1. Add your changes to the [CHANGELOG.md](CHANGELOG.md):
- 1. If you are fixing a ~regression issue, you can add your entry to the next
- patch release (e.g. `8.12.5` if current version is `8.12.4`)
- 1. Otherwise, add your entry to the next minor release (e.g. `8.13.0` if
- current version is `8.12.4`
- 1. Please add your entry at a random place among the entries of the targeted
- release
+1. [Generate a changelog entry with `bin/changelog`][changelog]
1. If you are writing documentation, make sure to follow the
[documentation styleguide][doc-styleguide]
1. If you have multiple commits please combine them into one commit by
@@ -262,8 +255,11 @@ request is as follows:
1. Submit a merge request (MR) to the `master` branch
1. The MR title should describe the change you want to make
1. The MR description should give a motive for your change and the method you
- used to achieve it, see the [merge request description format]
- (#merge-request-description-format)
+ used to achieve it.
+ 1. If you are contributing code, fill in the template already provided in the
+ "Description" field.
+ 1. If you are contributing documentation, choose `Documentation` from the
+ "Choose a template" menu and fill in the template.
1. If the MR changes the UI it should include *Before* and *After* screenshots
1. If the MR changes CSS classes please include the list of affected pages,
`grep css-class ./app -R`
@@ -469,6 +465,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[contributor-covenant]: http://contributor-covenant.org
[rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout
[rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming
+[changelog]: doc/development/changelog.md "Generate a changelog entry"
[doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide"
[scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide"
[newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide"
diff --git a/Gemfile b/Gemfile
index 7e94c4051da..af82ae16a56 100644
--- a/Gemfile
+++ b/Gemfile
@@ -117,7 +117,7 @@ gem 'truncato', '~> 0.7.8'
gem 'nokogiri', '~> 1.6.7', '>= 1.6.7.2'
# Diffs
-gem 'diffy', '~> 3.0.3'
+gem 'diffy', '~> 3.1.0'
# Application server
group :unicorn do
@@ -196,7 +196,7 @@ gem 'loofah', '~> 2.0.3'
gem 'licensee', '~> 8.0.0'
# Protect against bruteforcing
-gem 'rack-attack', '~> 4.3.1'
+gem 'rack-attack', '~> 4.4.1'
# Ace editor
gem 'ace-rails-ap', '~> 4.1.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 81af1ad2dac..888fa6b2bf5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -180,7 +180,7 @@ GEM
railties
rotp (~> 2.0)
diff-lcs (1.2.5)
- diffy (3.0.7)
+ diffy (3.1.0)
docile (1.1.5)
doorkeeper (4.2.0)
railties (>= 4.2)
@@ -520,7 +520,7 @@ GEM
rack (1.6.4)
rack-accept (0.4.5)
rack (>= 0.4)
- rack-attack (4.3.1)
+ rack-attack (4.4.1)
rack
rack-cors (0.4.0)
rack-mount (0.8.3)
@@ -847,7 +847,7 @@ DEPENDENCIES
default_value_for (~> 3.0.0)
devise (~> 4.2)
devise-two-factor (~> 3.0.0)
- diffy (~> 3.0.3)
+ diffy (~> 3.1.0)
doorkeeper (~> 4.2.0)
dropzonejs-rails (~> 0.7.1)
email_reply_parser (~> 0.5.8)
@@ -929,7 +929,7 @@ DEPENDENCIES
poltergeist (~> 1.9.0)
premailer-rails (~> 1.9.0)
pry-rails (~> 0.3.4)
- rack-attack (~> 4.3.1)
+ rack-attack (~> 4.4.1)
rack-cors (~> 0.4.0)
rack-oauth2 (~> 1.2.1)
rails (= 4.2.7.1)
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index e57cf1b3a58..7dd9adac736 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -1,6 +1,6 @@
/* eslint-disable */
// This is a manifest file that'll be compiled into including all the files listed below.
-// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
+// Add new JavaScript code in separate files in this directory and they'll automatically
// be included in the compiled file accessible from http://example.com/assets/application.js
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// the compiled file.
@@ -188,6 +188,7 @@
// Close select2 on escape
});
// Initialize tooltips
+ $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover';
$body.tooltip({
selector: '.has-tooltip, [data-toggle="tooltip"]',
placement: function(_, el) {
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 e520170ef74..db9a5a8e40a 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
@@ -22,7 +22,7 @@
fallbackClass: 'is-dragging',
fallbackOnBody: true,
ghostClass: 'is-ghost',
- filter: '.has-tooltip, .btn',
+ filter: '.board-delete, .btn',
delay: gl.issueBoards.touchEnabled ? 100 : 50,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20,
diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js
index 056baf66525..e103748d499 100644
--- a/app/assets/javascripts/graphs/graphs_bundle.js
+++ b/app/assets/javascripts/graphs/graphs_bundle.js
@@ -1,6 +1,6 @@
/* eslint-disable */
// This is a manifest file that'll be compiled into including all the files listed below.
-// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
+// Add new JavaScript code in separate files in this directory and they'll automatically
// be included in the compiled file accessible from http://example.com/assets/application.js
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// the compiled file.
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 6658e4811ce..860ee5df57e 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -238,8 +238,11 @@
_this.expandViewContainer();
}
_this.diffsLoaded = true;
- _this.scrollToElement("#diffs");
- _this.highlighSelectedLine();
+ var anchoredDiff = gl.utils.getLocationHash();
+ if (anchoredDiff) _this.openAnchoredDiff(anchoredDiff, function() {
+ _this.scrollToElement("#diffs");
+ _this.highlighSelectedLine();
+ });
_this.filesCommentButton = $('.files .diff-file').filesCommentButton();
return $(document).off('click', '.diff-line-num a').on('click', '.diff-line-num a', function(e) {
e.preventDefault();
@@ -252,6 +255,17 @@
});
};
+ MergeRequestTabs.prototype.openAnchoredDiff = function(anchoredDiff, cb) {
+ var diffTitle = $('#file-path-' + anchoredDiff);
+ var diffFile = diffTitle.closest('.diff-file');
+ var nothingHereBlock = $('.nothing-here-block:visible', diffFile);
+ if (nothingHereBlock.length) {
+ diffFile.singleFileDiff(true, cb);
+ } else {
+ cb();
+ }
+ };
+
MergeRequestTabs.prototype.highlighSelectedLine = function() {
var $diffLine, diffLineTop, hashClassString, locationHash, navBarHeight;
$('.hll').removeClass('hll');
diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js
index ede72a96d76..42d6799c82f 100644
--- a/app/assets/javascripts/network/network_bundle.js
+++ b/app/assets/javascripts/network/network_bundle.js
@@ -1,6 +1,6 @@
/* eslint-disable */
// This is a manifest file that'll be compiled into including all the files listed below.
-// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
+// Add new JavaScript code in separate files in this directory and they'll automatically
// be included in the compiled file accessible from http://example.com/assets/application.js
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// the compiled file.
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index adca76ddd5f..8e54ca4f0dc 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -13,7 +13,7 @@
COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>';
- function SingleFileDiff(file) {
+ function SingleFileDiff(file, forceLoad, cb) {
this.file = file;
this.toggleDiff = bind(this.toggleDiff, this);
this.content = $('.diff-content', this.file);
@@ -32,9 +32,12 @@
this.$toggleIcon.addClass('fa-caret-down');
}
$('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff);
+ if (forceLoad) {
+ this.toggleDiff(null, cb);
+ }
}
- SingleFileDiff.prototype.toggleDiff = function(e) {
+ SingleFileDiff.prototype.toggleDiff = function(e, cb) {
var $target = $(e.target);
if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
this.isOpen = !this.isOpen;
@@ -54,11 +57,11 @@
}
} else {
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
- return this.getContentHTML();
+ return this.getContentHTML(cb);
}
};
- SingleFileDiff.prototype.getContentHTML = function() {
+ SingleFileDiff.prototype.getContentHTML = function(cb) {
this.collapsedContent.hide();
this.loadingContent.show();
$.get(this.diffForPath, (function(_this) {
@@ -76,6 +79,8 @@
if (typeof DiffNotesApp !== 'undefined') {
DiffNotesApp.compileComponents();
}
+
+ if (cb) cb();
};
})(this));
};
@@ -84,10 +89,10 @@
})();
- $.fn.singleFileDiff = function() {
+ $.fn.singleFileDiff = function(forceLoad, cb) {
return this.each(function() {
- if (!$.data(this, 'singleFileDiff')) {
- return $.data(this, 'singleFileDiff', new SingleFileDiff(this));
+ if (!$.data(this, 'singleFileDiff') || forceLoad) {
+ return $.data(this, 'singleFileDiff', new SingleFileDiff(this, forceLoad, cb));
}
});
};
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
index a18d16ea46c..cfd1e2204d5 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -11,11 +11,9 @@
$this.parent().find('.star-count').text(data.star_count);
if (isStarred) {
$starSpan.removeClass('starred').text('Star');
- gl.utils.updateTooltipTitle($this, 'Star project');
$starIcon.removeClass('fa-star').addClass('fa-star-o');
} else {
$starSpan.addClass('starred').text('Unstar');
- gl.utils.updateTooltipTitle($this, 'Unstar project');
$starIcon.removeClass('fa-star-o').addClass('fa-star');
}
};
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 98e301d3799..ce117c3fba5 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -1,11 +1,36 @@
-.avatar {
+@mixin avatar-size($size, $margin-right) {
+ width: $size;
+ height: $size;
+ margin-right: $margin-right;
+}
+
+.avatar-container {
float: left;
- margin-right: 12px;
+ margin-right: 15px;
+ border-radius: $avatar_radius;
+ border: 1px solid rgba(0, 0, 0, .1);
+ &.s16 { @include avatar-size(16px, 6px); }
+ &.s20 { @include avatar-size(20px, 7px); }
+ &.s24 { @include avatar-size(24px, 8px); }
+ &.s26 { @include avatar-size(26px, 8px); }
+ &.s32 { @include avatar-size(32px, 10px); }
+ &.s36 { @include avatar-size(36px, 10px); }
+ &.s40 { @include avatar-size(40px, 10px); }
+ &.s46 { @include avatar-size(46px, 15px); }
+ &.s48 { @include avatar-size(48px, 10px); }
+ &.s60 { @include avatar-size(60px, 12px); }
+ &.s70 { @include avatar-size(70px, 14px); }
+ &.s90 { @include avatar-size(90px, 15px); }
+ &.s110 { @include avatar-size(110px, 15px); }
+ &.s140 { @include avatar-size(140px, 15px); }
+ &.s160 { @include avatar-size(160px, 20px); }
+}
+
+.avatar {
+ @extend .avatar-container;
width: 40px;
height: 40px;
padding: 0;
- border-radius: $avatar_radius;
- border: 1px solid rgba(0, 0, 0, .1);
&.avatar-inline {
float: none;
@@ -20,22 +45,6 @@
border-radius: 0;
border: none;
}
-
- &.s16 { width: 16px; height: 16px; margin-right: 6px; }
- &.s20 { width: 20px; height: 20px; margin-right: 7px; }
- &.s24 { width: 24px; height: 24px; margin-right: 8px; }
- &.s26 { width: 26px; height: 26px; margin-right: 8px; }
- &.s32 { width: 32px; height: 32px; margin-right: 10px; }
- &.s36 { width: 36px; height: 36px; margin-right: 10px; }
- &.s40 { width: 40px; height: 40px; margin-right: 10px; }
- &.s46 { width: 46px; height: 46px; margin-right: 15px; }
- &.s48 { width: 48px; height: 48px; margin-right: 10px; }
- &.s60 { width: 60px; height: 60px; margin-right: 12px; }
- &.s70 { width: 70px; height: 70px; margin-right: 14px; }
- &.s90 { width: 90px; height: 90px; margin-right: 15px; }
- &.s110 { width: 110px; height: 110px; margin-right: 15px; }
- &.s140 { width: 140px; height: 140px; margin-right: 20px; }
- &.s160 { width: 160px; height: 160px; margin-right: 20px; }
}
.identicon {
@@ -54,3 +63,17 @@
&.s140 { font-size: 72px; line-height: 138px; }
&.s160 { font-size: 96px; line-height: 158px; }
}
+
+.image-container {
+ @extend .avatar-container;
+ overflow: hidden;
+ display: flex;
+
+ .avatar {
+ border-radius: 0;
+ border: none;
+ height: auto;
+ margin: 0;
+ align-self: center;
+ }
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index c0e9c8bf829..ed21ad83a1c 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -216,7 +216,7 @@
svg,
.fa {
&:not(:last-child) {
- margin-right: 3px;
+ margin-right: 5px;
}
}
}
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index 3f877d86a26..91ab1503439 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -21,57 +21,66 @@
background: $color-darker;
}
- .nav-sidebar li {
- a {
- color: $color-light;
-
- &:hover,
- &:focus,
- &:active {
- background: $color-dark;
- }
+ .sidebar-header,
+ .sidebar-action-buttons {
+ color: $color-light;
+ background-color: lighten($color-darker, 5%);
+ }
- i {
+ .nav-sidebar {
+ li {
+ a {
color: $color-light;
- }
-
- path,
- polygon {
- fill: $color-light;
- }
- .count {
- color: $color-light;
- background: $color-dark;
+ &:hover,
+ &:focus,
+ &:active {
+ background: $color-dark;
+ }
+
+ i {
+ color: $color-light;
+ }
+
+ path,
+ polygon {
+ fill: $color-light;
+ }
+
+ .count {
+ color: $color-light;
+ background: $color-dark;
+ }
+
+ svg {
+ position: relative;
+ top: 3px;
+ }
}
- svg {
- position: relative;
- top: 3px;
+ &.separate-item {
+ border-top: 1px solid $color;
}
- }
-
- &.separate-item {
- border-top: 1px solid $color;
- }
- &.active a {
- color: $white-light;
- background: $color-dark;
+ &.active a {
+ color: $white-light;
+ background: $color-dark;
- &.no-highlight {
- border: none;
- }
+ &.no-highlight {
+ border: none;
+ }
- i {
- color: $white-light;
- }
+ i {
+ color: $white-light;
+ }
- path,
- polygon {
- fill: $white-light;
+ path,
+ polygon {
+ fill: $white-light;
+ }
}
}
+
}
}
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 53ee1ed309e..4993ca7572a 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -49,12 +49,16 @@ header {
font-size: 18px;
padding: 0;
margin: ($header-height - 28) / 2 0;
- margin-left: 10px;
+ margin-left: 8px;
height: 28px;
min-width: 28px;
line-height: 28px;
text-align: center;
+ &.header-user-dropdown-toggle {
+ margin-left: 14px;
+ }
+
&:hover,
&:focus,
&:active {
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 78464af94bd..bc0610cc417 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -142,10 +142,6 @@ ul.content-list {
}
}
- .avatar {
- margin-right: 15px;
- }
-
.controls {
float: right;
diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss
index b6f21fd8c91..cb2c351c368 100644
--- a/app/assets/stylesheets/framework/pagination.scss
+++ b/app/assets/stylesheets/framework/pagination.scss
@@ -7,8 +7,70 @@
.pagination {
padding: 0;
}
+
+ .gap,
+ .gap:hover {
+ background-color: $gray-light;
+ padding: $gl-vert-padding;
+ cursor: default;
+ }
}
.panel > .gl-pagination {
margin: 0;
}
+
+/**
+ * Extra-small screen pagination.
+ */
+@media (max-width: 320px) {
+ .gl-pagination {
+ .first,
+ .last {
+ display: none;
+ }
+
+ .page {
+ display: none;
+
+ &.active {
+ display: inline;
+ }
+ }
+ }
+}
+
+/**
+ * Small screen pagination
+ */
+@media (max-width: $screen-xs) {
+ .gl-pagination {
+ .pagination li a {
+ padding: 6px 10px;
+ }
+
+ .page {
+ display: none;
+
+ &.active {
+ display: inline;
+ }
+ }
+ }
+}
+
+/**
+ * Medium screen pagination
+ */
+@media (min-width: $screen-xs) and (max-width: $screen-md-max) {
+ .gl-pagination {
+ .page {
+ display: none;
+
+ &.active,
+ &.sibling {
+ display: inline;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index c54f7b27575..d74c14ee2a4 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -59,6 +59,11 @@
padding: 0 !important;
}
+ .sidebar-header {
+ padding: 11px 22px 12px;
+ font-size: 20px;
+ }
+
li {
&.separate-item {
padding-top: 10px;
diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss
index 9282e0ae03b..486ad16ea26 100644
--- a/app/assets/stylesheets/pages/awards.scss
+++ b/app/assets/stylesheets/pages/awards.scss
@@ -1,7 +1,7 @@
.awards {
.emoji-icon {
- width: 20px;
- height: 20px;
+ width: 19px;
+ height: 19px;
}
}
@@ -94,7 +94,7 @@
.award-control {
margin: 3px 5px 3px 0;
- padding: 6px 5px;
+ padding: 5px 6px;
outline: 0;
&:hover,
@@ -127,7 +127,7 @@
.award-control-icon {
float: left;
margin-right: 5px;
- font-size: 20px;
+ font-size: 19px;
}
.award-control-icon-loading {
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 608804c63ec..47a7e84b5c6 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -49,11 +49,6 @@
.page-with-sidebar {
padding-bottom: 0;
}
-
- .issues-filters {
- position: relative;
- z-index: 999999;
- }
}
.boards-app {
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index d6a55fbd464..6300ac9662f 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -52,10 +52,25 @@
.build-header {
position: relative;
- padding-right: 40px;
+ padding: 0;
+ display: flex;
+ min-height: 58px;
+ align-items: center;
- @media (min-width: $screen-sm-min) {
- padding-right: 0;
+ .btn-inverted {
+ @include btn-outline($white-light, $blue-normal, $blue-normal, $blue-light, $white-light, $blue-light);
+ }
+
+ @media (max-width: $screen-sm-max) {
+ padding-right: 40px;
+
+ .btn-inverted {
+ display: none;
+ }
+ }
+
+ .header-content {
+ flex: 1;
}
a {
@@ -137,10 +152,15 @@
.retry-link {
color: $gl-link-color;
+ display: none;
&:hover {
text-decoration: underline;
}
+
+ @media (max-width: $screen-sm-max) {
+ display: block;
+ }
}
.stage-item {
diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss
index 76225ed8d06..016bab104eb 100644
--- a/app/assets/stylesheets/pages/dashboard.scss
+++ b/app/assets/stylesheets/pages/dashboard.scss
@@ -36,10 +36,6 @@
}
}
-.dash-project-avatar {
- float: left;
-}
-
.dash-project-access-icon {
float: left;
margin-right: 5px;
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index ee2a398f031..4375e29c8db 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -3,12 +3,14 @@
}
.dashboard .side .panel .panel-heading .input-group {
+
.form-control {
height: 42px;
}
}
.group-row {
+
.stats {
float: right;
line-height: $list-text-height;
@@ -21,12 +23,14 @@
}
.ldap-group-links {
+
.form-actions {
margin-bottom: $gl-padding;
}
}
.groups-cover-block {
+
.container-fluid {
position: relative;
}
@@ -41,9 +45,14 @@
background-color: $background-color;
}
}
+
+ .group-avatar {
+ border: 0;
+ }
}
.groups-header {
+
@media (min-width: $screen-sm-min) {
.nav-links {
width: 35%;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index b90c91831f2..526e9ae5cdd 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -105,11 +105,6 @@ ul.notes {
padding: 2px;
margin-top: 10px;
}
-
- .award-control {
- font-size: 13px;
- padding: 2px 5px;
- }
}
.note-header {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index f0d39b353d2..f7d54564530 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -96,8 +96,8 @@
.project-avatar {
float: none;
- margin-left: auto;
- margin-right: auto;
+ margin: 0 auto;
+ border: none;
&.identicon {
border-radius: 50%;
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index ea76fe18876..b3aef2fdd32 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -161,3 +161,63 @@
}
}
}
+
+.todos-empty {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ max-width: 900px;
+ margin-left: auto;
+ margin-right: auto;
+
+ @media (min-width: $screen-sm-min) {
+ -webkit-flex-direction: row;
+ flex-direction: row;
+ padding-top: 80px;
+ }
+}
+
+.todos-empty-content {
+ -webkit-align-self: center;
+ align-self: center;
+ max-width: 480px;
+ margin-right: 20px;
+}
+
+.todos-empty-hero {
+ width: 200px;
+ margin-left: auto;
+ margin-right: auto;
+
+ @media (min-width: $screen-sm-min) {
+ width: 300px;
+ margin-right: 0;
+ -webkit-order: 2;
+ order: 2;
+ }
+}
+
+.todos-all-done {
+ padding-top: 20px;
+
+ @media (min-width: $screen-sm-min) {
+ padding-top: 50px;
+ }
+
+ > svg {
+ display: block;
+ max-width: 300px;
+ margin: 0 auto 20px;
+ }
+
+ p {
+ max-width: 470px;
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ a {
+ font-weight: 600;
+ }
+}
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index f35f4a8c811..bb912ed10cc 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -3,7 +3,7 @@ class Admin::UsersController < Admin::ApplicationController
def index
@users = User.order_name_asc.filter(params[:filter])
- @users = @users.search(params[:name]) if params[:name].present?
+ @users = @users.search_with_secondary_emails(params[:search_query]) if params[:search_query].present?
@users = @users.sort(@sort = params[:sort])
@users = @users.page(params[:page])
end
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 4f855134368..42fd09e9b7e 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -126,7 +126,7 @@ class Projects::LabelsController < Projects::ApplicationController
alias_method :subscribable_resource, :label
def find_labels
- @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute.includes(:priorities)
+ @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
end
def authorize_admin_labels!
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index d08f490de18..699a56ae2f8 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -25,18 +25,15 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def create
- if params[:user_ids].blank?
- return redirect_to(namespace_project_project_members_path(@project.namespace, @project), alert: 'No users or groups specified.')
- end
+ status = Members::CreateService.new(@project, current_user, params).execute
- @project.team.add_users(
- params[:user_ids].split(','),
- params[:access_level],
- expires_at: params[:expires_at],
- current_user: current_user
- )
+ redirect_url = namespace_project_project_members_path(@project.namespace, @project)
- redirect_to namespace_project_project_members_path(@project.namespace, @project), notice: 'Users were successfully added.'
+ if status
+ redirect_to redirect_url, notice: 'Users were successfully added.'
+ else
+ redirect_to redirect_url, alert: 'No users or groups specified.'
+ end
end
def update
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 8c148ecfaeb..bce5e29d8d8 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -289,7 +289,8 @@ class ProjectsController < Projects::ApplicationController
render 'projects/empty' if @project.empty_repo?
else
if @project.wiki_enabled?
- @wiki_home = @project.wiki.find_page('home', params[:version_id])
+ @project_wiki = @project.wiki
+ @wiki_home = @project_wiki.find_page('home', params[:version_id])
elsif @project.feature_available?(:issues, current_user)
@issues = issues_collection
@issues = @issues.page(params[:page])
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index e27986ef95b..cc2073081b5 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -126,7 +126,7 @@ class IssuableFinder
@labels =
if labels? && !filter_by_no_label?
- LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute
+ LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute(skip_authorization: true)
else
Label.none
end
@@ -273,7 +273,7 @@ class IssuableFinder
items = items.with_label(label_names, params[:sort])
if projects
- label_ids = LabelsFinder.new(current_user, project_ids: projects).execute.select(:id)
+ label_ids = LabelsFinder.new(current_user, project_ids: projects).execute(skip_authorization: true).select(:id)
items = items.where(labels: { id: label_ids })
end
end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index f8ded05c31a..00e64076408 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -39,6 +39,12 @@ module EventsHelper
end
end
+ def event_filter_visible(feature_key)
+ return true unless @project
+
+ @project.feature_available?(feature_key, current_user)
+ end
+
def event_preposition(event)
if event.push? || event.commented? || event.target
"at"
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index 9216122923e..6d88951c713 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -31,7 +31,7 @@ module ProjectFeaturesCompatibility
def write_feature_attribute(field, value)
build_project_feature unless project_feature
- access_level = value == "true" ? ProjectFeature::ENABLED : ProjectFeature::DISABLED
+ access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED
project_feature.update_attribute(field, access_level)
end
end
diff --git a/app/models/group_label.rb b/app/models/group_label.rb
index a698b532d19..68841ace2e6 100644
--- a/app/models/group_label.rb
+++ b/app/models/group_label.rb
@@ -5,6 +5,10 @@ class GroupLabel < Label
alias_attribute :subject, :group
+ def subject_foreign_key
+ 'group_id'
+ end
+
def to_reference(source_project = nil, target_project = nil, format: :id)
super(source_project, target_project, format: format)
end
diff --git a/app/models/label.rb b/app/models/label.rb
index 149fd98ecb3..d9287f2dc29 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -92,16 +92,23 @@ class Label < ActiveRecord::Base
nil
end
- def open_issues_count(user = nil, project = nil)
- issues_count(user, project_id: project.try(:id) || project_id, state: 'opened')
+ def open_issues_count(user = nil)
+ issues_count(user, state: 'opened')
end
- def closed_issues_count(user = nil, project = nil)
- issues_count(user, project_id: project.try(:id) || project_id, state: 'closed')
+ def closed_issues_count(user = nil)
+ issues_count(user, state: 'closed')
end
- def open_merge_requests_count(user = nil, project = nil)
- merge_requests_count(user, project_id: project.try(:id) || project_id, state: 'opened')
+ def open_merge_requests_count(user = nil)
+ params = {
+ subject_foreign_key => subject.id,
+ label_name: title,
+ scope: 'all',
+ state: 'opened'
+ }
+
+ MergeRequestsFinder.new(user, params.with_indifferent_access).execute.count
end
def prioritize!(project, value)
@@ -167,15 +174,8 @@ class Label < ActiveRecord::Base
end
def issues_count(user, params = {})
- IssuesFinder.new(user, params.reverse_merge(label_name: title, scope: 'all'))
- .execute
- .count
- end
-
- def merge_requests_count(user, params = {})
- MergeRequestsFinder.new(user, params.reverse_merge(label_name: title, scope: 'all'))
- .execute
- .count
+ params.merge!(subject_foreign_key => subject.id, label_name: title, scope: 'all')
+ IssuesFinder.new(user, params.with_indifferent_access).execute.count
end
def label_format_reference(format = :id)
diff --git a/app/models/project.rb b/app/models/project.rb
index ae57815c620..d5512dfaf9c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -30,6 +30,11 @@ class Project < ActiveRecord::Base
default_value_for :container_registry_enabled, gitlab_config_features.container_registry
default_value_for(:repository_storage) { current_application_settings.repository_storage }
default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
+ default_value_for :issues_enabled, gitlab_config_features.issues
+ default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests
+ default_value_for :builds_enabled, gitlab_config_features.builds
+ default_value_for :wiki_enabled, gitlab_config_features.wiki
+ default_value_for :snippets_enabled, gitlab_config_features.snippets
after_create :ensure_dir_exist
after_create :create_project_feature, unless: :project_feature
@@ -390,7 +395,7 @@ class Project < ActiveRecord::Base
end
def group_ids
- joins(:namespace).where(namespaces: { type: 'Group' }).pluck(:namespace_id)
+ joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
end
end
diff --git a/app/models/project_label.rb b/app/models/project_label.rb
index 33c2b617715..82f47f0e8fd 100644
--- a/app/models/project_label.rb
+++ b/app/models/project_label.rb
@@ -12,6 +12,10 @@ class ProjectLabel < Label
alias_attribute :subject, :project
+ def subject_foreign_key
+ 'project_id'
+ end
+
def to_reference(target_project = nil, format: :id)
super(project, target_project, format: format)
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 5bcf199d468..0a493b7a12b 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -237,7 +237,7 @@ class JiraService < IssueTrackerService
end
def resource_url(resource)
- "#{Settings.gitlab['url'].chomp("/")}#{resource}"
+ "#{Settings.gitlab.base_url.chomp("/")}#{resource}"
end
def build_entity_url(entity_name, entity_id)
diff --git a/app/models/user.rb b/app/models/user.rb
index e2a97c3a757..af3c0b7dc02 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -258,6 +258,24 @@ class User < ActiveRecord::Base
)
end
+ # searches user by given pattern
+ # it compares name, email, username fields and user's secondary emails with given pattern
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+
+ def search_with_secondary_emails(query)
+ table = arel_table
+ email_table = Email.arel_table
+ pattern = "%#{query}%"
+ matched_by_emails_user_ids = email_table.project(email_table[:user_id]).where(email_table[:email].matches(pattern))
+
+ where(
+ table[:name].matches(pattern).
+ or(table[:email].matches(pattern)).
+ or(table[:username].matches(pattern)).
+ or(table[:id].in(matched_by_emails_user_ids))
+ )
+ end
+
def by_login(login)
return nil unless login
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index fbb3d4507d6..1ee31023e26 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -2,11 +2,11 @@ class ProjectPolicy < BasePolicy
def rules
team_access!(user)
- owner = user.admin? ||
- project.owner == user ||
+ owner = project.owner == user ||
(project.group && project.group.has_owner?(user))
- owner_access! if owner
+ owner_access! if user.admin? || owner
+ team_member_owner_access! if owner
if project.public? || (project.internal? && !user.external?)
guest_access!
@@ -16,7 +16,7 @@ class ProjectPolicy < BasePolicy
can! :read_build if project.public_builds?
if project.request_access_enabled &&
- !(owner || project.team.member?(user) || project_group_member?(user))
+ !(owner || user.admin? || project.team.member?(user) || project_group_member?(user))
can! :request_access
end
end
@@ -135,6 +135,10 @@ class ProjectPolicy < BasePolicy
can! :destroy_issue
end
+ def team_member_owner_access!
+ team_member_reporter_access!
+ end
+
# Push abilities on the users team role
def team_access!(user)
access = project.team.max_member_access(user.id)
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
new file mode 100644
index 00000000000..e4b24ccef92
--- /dev/null
+++ b/app/services/members/create_service.rb
@@ -0,0 +1,16 @@
+module Members
+ class CreateService < BaseService
+ def execute
+ return false if params[:user_ids].blank?
+
+ project.team.add_users(
+ params[:user_ids].split(','),
+ params[:access_level],
+ expires_at: params[:expires_at],
+ current_user: current_user
+ )
+
+ true
+ end
+ end
+end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 404f75616b5..f415244068b 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -13,20 +13,8 @@ module MergeRequests
merge_request.target_project ||= (project.forked_from_project || project)
merge_request.target_branch ||= merge_request.target_project.default_branch
- if merge_request.target_branch.blank? || merge_request.source_branch.blank?
- message =
- if params[:source_branch] || params[:target_branch]
- "You must select source and target branch"
- end
-
- return build_failed(merge_request, message)
- end
-
- if merge_request.source_project == merge_request.target_project &&
- merge_request.target_branch == merge_request.source_branch
-
- return build_failed(merge_request, 'You must select different branches')
- end
+ messages = validate_branches(merge_request)
+ return build_failed(merge_request, messages) unless messages.empty?
compare = CompareService.new.execute(
merge_request.source_project,
@@ -43,6 +31,34 @@ module MergeRequests
private
+ def validate_branches(merge_request)
+ messages = []
+
+ if merge_request.target_branch.blank? || merge_request.source_branch.blank?
+ messages <<
+ if params[:source_branch] || params[:target_branch]
+ "You must select source and target branch"
+ end
+ end
+
+ if merge_request.source_project == merge_request.target_project &&
+ merge_request.target_branch == merge_request.source_branch
+
+ messages << 'You must select different branches'
+ end
+
+ # See if source and target branches exist
+ unless merge_request.source_project.commit(merge_request.source_branch)
+ messages << "Source branch \"#{merge_request.source_branch}\" does not exist"
+ end
+
+ unless merge_request.target_project.commit(merge_request.target_branch)
+ messages << "Target branch \"#{merge_request.target_branch}\" does not exist"
+ end
+
+ messages
+ end
+
# When your branch name starts with an iid followed by a dash this pattern will be
# interpreted as the user wants to close that issue on this project.
#
@@ -91,8 +107,10 @@ module MergeRequests
merge_request
end
- def build_failed(merge_request, message)
- merge_request.errors.add(:base, message) unless message.nil?
+ def build_failed(merge_request, messages)
+ messages.compact.each do |message|
+ merge_request.errors.add(:base, message)
+ end
merge_request.compare_commits = []
merge_request.can_be_created = false
merge_request
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index adfa1eaafc9..05c88ca1cc8 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -16,7 +16,8 @@
%span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
= visibility_level_icon(group.visibility_level, fw: false)
- = image_tag group_icon(group), class: "avatar s40 hidden-xs"
+ .image-container.s40
+ = image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title
= link_to [:admin, group], class: 'group-name' do
= group.name
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 0188ed448ce..a7c1a4f5038 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -13,7 +13,8 @@
Group info:
%ul.well-list
%li
- = image_tag group_icon(@group), class: "avatar s60"
+ .image-container.s60
+ = image_tag group_icon(@group), class: "avatar s60"
%li
%span.light Name:
%strong= @group.name
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index 339cfc613fe..10dce6f3d8f 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -76,7 +76,8 @@
.title
= link_to [:admin, project.namespace.becomes(Namespace), project] do
.dash-project-avatar
- = project_icon(project, alt: '', class: 'avatar project-avatar s40')
+ .image-container.s40
+ = project_icon(project, alt: '', class: 'avatar project-avatar s40')
%span.project-full-name
%span.namespace-name
- if project.namespace
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index 357123c2c13..d3038ae644f 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -10,7 +10,7 @@
= hidden_field_tag "filter", h(params[:filter])
.search-holder
.search-field-holder
- = search_field_tag :name, params[:name], placeholder: 'Search by name, email or username', class: 'form-control search-text-input js-search-input', spellcheck: false
+ = search_field_tag :search_query, params[:search_query], placeholder: 'Search by name, email or username', class: 'form-control search-text-input js-search-input', spellcheck: false
= icon("search", class: "search-icon")
.dropdown
- toggle_text = if @sort.present? then sort_options_hash[@sort] else sort_title_name end
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 2a0302638ba..2411cc45724 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -1,69 +1,70 @@
- page_title "Todos"
- header_title "Todos", dashboard_todos_path
-.top-area
- %ul.nav-links
- - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending')
- %li{class: "todos-pending #{todo_pending_active}"}
- = link_to todos_filter_path(state: 'pending') do
- %span
- To do
- %span.badge
- = number_with_delimiter(todos_pending_count)
- - todo_done_active = ('active' if params[:state] == 'done')
- %li{class: "todos-done #{todo_done_active}"}
- = link_to todos_filter_path(state: 'done') do
- %span
- Done
- %span.badge
- = number_with_delimiter(todos_done_count)
+- if current_user.todos.any?
+ .top-area
+ %ul.nav-links
+ - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending')
+ %li{class: "todos-pending #{todo_pending_active}"}
+ = link_to todos_filter_path(state: 'pending') do
+ %span
+ To do
+ %span.badge
+ = number_with_delimiter(todos_pending_count)
+ - todo_done_active = ('active' if params[:state] == 'done')
+ %li{class: "todos-done #{todo_done_active}"}
+ = link_to todos_filter_path(state: 'done') do
+ %span
+ Done
+ %span.badge
+ = number_with_delimiter(todos_done_count)
- .nav-controls
- - if @todos.any?(&:pending?)
- = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete do
- Mark all as done
- = icon('spinner spin')
+ .nav-controls
+ - if @todos.any?(&:pending?)
+ = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete do
+ Mark all as done
+ = icon('spinner spin')
-.todos-filters
- .row-content-block.second-block
- = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do
- .filter-item.inline
- - if params[:project_id].present?
- = hidden_field_tag(:project_id, params[:project_id])
- = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
- placeholder: 'Search projects', data: { data: todo_projects_options } })
- .filter-item.inline
- - if params[:author_id].present?
- = hidden_field_tag(:author_id, params[:author_id])
- = dropdown_tag(user_dropdown_label(params[:author_id], 'Author'), options: { toggle_class: 'js-user-search js-filter-submit js-author-search', title: 'Filter by author', filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit',
- placeholder: 'Search authors', data: { any_user: 'Any Author', first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: 'Author' } })
- .filter-item.inline
- - if params[:type].present?
- = hidden_field_tag(:type, params[:type])
- = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit',
- data: { data: todo_types_options } })
- .filter-item.inline.actions-filter
- - if params[:action_id].present?
- = hidden_field_tag(:action_id, params[:action_id])
- = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit',
- data: { data: todo_actions_options }})
- .pull-right
- .dropdown.inline.prepend-left-10
- %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
- %span.light
- - if @sort.present?
- = sort_options_hash[@sort]
- - else
- = sort_title_recently_created
- = icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
- %li
- = link_to todos_filter_path(sort: sort_value_priority) do
- = sort_title_priority
- = link_to todos_filter_path(sort: sort_value_recently_created) do
+ .todos-filters
+ .row-content-block.second-block
+ = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do
+ .filter-item.inline
+ - if params[:project_id].present?
+ = hidden_field_tag(:project_id, params[:project_id])
+ = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
+ placeholder: 'Search projects', data: { data: todo_projects_options } })
+ .filter-item.inline
+ - if params[:author_id].present?
+ = hidden_field_tag(:author_id, params[:author_id])
+ = dropdown_tag(user_dropdown_label(params[:author_id], 'Author'), options: { toggle_class: 'js-user-search js-filter-submit js-author-search', title: 'Filter by author', filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit',
+ placeholder: 'Search authors', data: { any_user: 'Any Author', first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: 'Author' } })
+ .filter-item.inline
+ - if params[:type].present?
+ = hidden_field_tag(:type, params[:type])
+ = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit',
+ data: { data: todo_types_options } })
+ .filter-item.inline.actions-filter
+ - if params[:action_id].present?
+ = hidden_field_tag(:action_id, params[:action_id])
+ = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit',
+ data: { data: todo_actions_options }})
+ .pull-right
+ .dropdown.inline.prepend-left-10
+ %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+ %span.light
+ - if @sort.present?
+ = sort_options_hash[@sort]
+ - else
= sort_title_recently_created
- = link_to todos_filter_path(sort: sort_value_oldest_created) do
- = sort_title_oldest_created
+ = icon('caret-down')
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
+ %li
+ = link_to todos_filter_path(sort: sort_value_priority) do
+ = sort_title_priority
+ = link_to todos_filter_path(sort: sort_value_recently_created) do
+ = sort_title_recently_created
+ = link_to todos_filter_path(sort: sort_value_oldest_created) do
+ = sort_title_oldest_created
.prepend-top-default
@@ -78,5 +79,29 @@
%ul.content-list.todos-list
= render group[1]
= paginate @todos, theme: "gitlab"
+ - elsif current_user.todos.any?
+ .todos-all-done
+ = render "shared/empty_states/todos_all_done.svg"
+ %h4.text-center
+ Good job! Looks like you don't have any todos left.
+ %p.text-center
+ Are you looking for things to do? Take a look at
+ = succeed "," do
+ = link_to "the opened issues", issues_dashboard_path
+ contribute to
+ = link_to "merge requests", merge_requests_dashboard_path
+ or mention someone in a comment to assign a new todo automatically.
- else
- .nothing-here-block You're all done!
+ .todos-empty
+ .todos-empty-hero
+ = render "shared/empty_states/todos_empty.svg"
+ .todos-empty-content
+ %h4
+ Todos let you see what you should do next.
+ %p
+ When an issue or merge request is assigned to you, or when you
+ %strong
+ @mention
+ in a comment, this will trigger a new item in your todo list, automatically.
+ %p
+ You will always know what to work on next.
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index c766370d5a0..f84ac37fa8f 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -8,7 +8,8 @@
.form-group
.col-sm-offset-2.col-sm-10
- = image_tag group_icon(@group), alt: '', class: 'avatar group-avatar s160'
+ .image-container.s160
+ = image_tag group_icon(@group), alt: '', class: 'avatar group-avatar s160'
%p.light
- if @group.avatar?
You can change your group avatar here
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index 70783a63409..45325d6bc4b 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -13,7 +13,7 @@
.other-labels
- if @labels.present?
%ul.content-list.manage-labels-list.js-other-labels
- = render partial: 'shared/label', collection: @labels, as: :label
+ = render partial: 'shared/label', subject: @group, collection: @labels, as: :label
= paginate @labels, theme: 'gitlab'
- else
.nothing-here-block
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index fab61f447c2..275581b3af8 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -6,7 +6,8 @@
.cover-block.groups-cover-block
%div{ class: container_class }
- = image_tag group_icon(@group), class: "avatar group-avatar s70 avatar-tile"
+ .image-container.s70.group-avatar
+ = image_tag group_icon(@group), class: "avatar s70 avatar-tile"
.group-info
.cover-title
%h1
diff --git a/app/views/kaminari/gitlab/_gap.html.haml b/app/views/kaminari/gitlab/_gap.html.haml
index 80ca30f36e6..889514c4755 100644
--- a/app/views/kaminari/gitlab/_gap.html.haml
+++ b/app/views/kaminari/gitlab/_gap.html.haml
@@ -4,6 +4,6 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li{class: "page"}
- %span.page.gap
+%li
+ %span.gap
= raw(t 'views.pagination.truncate')
diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml
index 522e4d1d05f..750aed8f329 100644
--- a/app/views/kaminari/gitlab/_page.html.haml
+++ b/app/views/kaminari/gitlab/_page.html.haml
@@ -6,5 +6,5 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li{class: "page#{' active' if page.current?}"}
+%li{class: "page#{' active' if page.current?}#{' sibling' if page.next? || page.prev?}"}
= link_to page, url, {remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil}
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 7faa8bded86..7a9859262f7 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -29,10 +29,6 @@
= icon('bell fw')
%span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) }
= todos_pending_count
- - if current_user.can_create_project?
- %li
- = link_to new_project_path, title: 'New project', aria: { label: "New project" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('plus fw')
- if Gitlab::Sherlock.enabled?
%li
= link_to sherlock_transactions_path, title: 'Sherlock Transactions',
@@ -48,6 +44,8 @@
= link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username }
%li
= link_to "Profile Settings", profile_path, aria: { label: "Profile Settings" }
+ %li
+ = link_to "Help", help_path, aria: { label: "Help" }
%li.divider
%li
= link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link", aria: { label: "Sign out" }
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 3b1295dc3c0..a0356feef95 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,49 +1,38 @@
-%ul.nav.nav-sidebar
- = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do
- = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
- %span
- Projects
- = nav_link(controller: :todos) do
- = link_to dashboard_todos_path, title: 'Todos' do
- %span
- Todos
- %span.count.js-todos-count= number_with_delimiter(todos_pending_count)
- = nav_link(path: 'dashboard#activity') do
- = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
- %span
- Activity
- - if koding_enabled?
- = nav_link(controller: :koding) do
- = link_to koding_path, title: 'Koding' do
+.nav-sidebar
+ .sidebar-header Across GitLab
+ %ul.nav
+ = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do
+ = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
%span
- Koding
- = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
- = link_to dashboard_groups_path, title: 'Groups' do
- %span
- Groups
- = nav_link(controller: 'dashboard/milestones') do
- = link_to dashboard_milestones_path, title: 'Milestones' do
- %span
- Milestones
- = nav_link(path: 'dashboard#issues') do
- = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
- %span
- Issues
- %span.count= number_with_delimiter(current_user.assigned_issues.opened.count)
- = nav_link(path: 'dashboard#merge_requests') do
- = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
- %span
- Merge Requests
- %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
- = nav_link(controller: 'dashboard/snippets') do
- = link_to dashboard_snippets_path, title: 'Snippets' do
- %span
- Snippets
- = nav_link(controller: :help) do
- = link_to help_path, title: 'Help' do
- %span
- Help
- = nav_link(html_options: {class: profile_tab_class}) do
- = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do
- %span
- Profile Settings
+ Projects
+ = nav_link(path: 'dashboard#activity') do
+ = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
+ %span
+ Activity
+ - if koding_enabled?
+ = nav_link(controller: :koding) do
+ = link_to koding_path, title: 'Koding' do
+ %span
+ Koding
+ = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
+ = link_to dashboard_groups_path, title: 'Groups' do
+ %span
+ Groups
+ = nav_link(controller: 'dashboard/milestones') do
+ = link_to dashboard_milestones_path, title: 'Milestones' do
+ %span
+ Milestones
+ = nav_link(path: 'dashboard#issues') do
+ = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
+ %span
+ Issues
+ %span.count= number_with_delimiter(current_user.assigned_issues.opened.count)
+ = nav_link(path: 'dashboard#merge_requests') do
+ = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
+ %span
+ Merge Requests
+ %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
+ = nav_link(controller: 'dashboard/snippets') do
+ = link_to dashboard_snippets_path, title: 'Snippets' do
+ %span
+ Snippets
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index d3987fc9c4f..e67b66d1fff 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,7 +1,8 @@
- empty_repo = @project.empty_repo?
.project-home-panel.text-center{ class: ("empty-project" if empty_repo) }
%div{ class: container_class }
- = project_icon(@project, alt: @project.name, class: 'project-avatar avatar s70 avatar-tile')
+ .image-container.s70.project-avatar
+ = project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile')
%h1.project-title
= @project.name
%span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)}
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index ba1502c97b6..f7071051efc 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -11,7 +11,9 @@
.board-inner
%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.has-tooltip{ ":title" => "(list.label ? list.label.description : '')",
+ data: { container: "body", placement: "bottom" } }
+ {{ list.title }}
.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 }}
diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml
index 51b5bd9db42..3f2ce7377fd 100644
--- a/app/views/projects/builds/_header.html.haml
+++ b/app/views/projects/builds/_header.html.haml
@@ -1,16 +1,19 @@
.content-block.build-header
- = ci_status_with_icon(@build.status)
- Build
- %strong ##{@build.id}
- for commit
- = link_to ci_status_path(@build.pipeline) do
- %strong= @build.pipeline.short_sha
- from
- = link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do
- %code
- = @build.ref
- - if @build.user
- = render "user"
- = time_ago_with_tooltip(@build.created_at)
+ .header-content
+ = ci_status_with_icon(@build.status)
+ Build
+ %strong ##{@build.id}
+ for commit
+ = link_to ci_status_path(@build.pipeline) do
+ %strong= @build.pipeline.short_sha
+ from
+ = link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do
+ %code
+ = @build.ref
+ - if @build.user
+ = render "user"
+ = time_ago_with_tooltip(@build.created_at)
+ - if can?(current_user, :update_build, @build) && @build.retryable?
+ = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted pull-right', method: :post
%button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index b1053028279..28f519f11b2 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -44,7 +44,7 @@
.title
Build details
- if can?(current_user, :update_build, @build) && @build.retryable?
- = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post
+ = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post
- if @build.merge_request
%p.build-detail-row
%span.build-light-text Merge Request:
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index 29d549a60f5..27da86b9efe 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -5,10 +5,10 @@
= custom_icon('icon_fork')
%span Fork
- else
- = link_to new_namespace_project_fork_path(@project.namespace, @project), title: 'Fork project', class: 'btn has-tooltip' do
+ = link_to new_namespace_project_fork_path(@project.namespace, @project), title: 'Fork project', class: 'btn' do
= custom_icon('icon_fork')
%span Fork
%div.count-with-arrow
%span.arrow
- = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'count has-tooltip' do
+ = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'count' do
= @project.forks_count
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index 311583037e5..12d35101770 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -1,5 +1,5 @@
- if current_user
- = link_to toggle_star_namespace_project_path(@project.namespace, @project), { class: 'btn star-btn toggle-star has-tooltip', method: :post, remote: true, title: current_user.starred?(@project) ? 'Unstar project' : 'Star project' } do
+ = link_to toggle_star_namespace_project_path(@project.namespace, @project), { class: 'btn star-btn toggle-star', method: :post, remote: true } do
- if current_user.starred?(@project)
= icon('star')
%span.starred Unstar
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 30473d14b9b..c40ad06969e 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -118,7 +118,8 @@
Project avatar
.form-group
- if @project.avatar?
- = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160')
+ .image-container.s160
+ = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160')
%p.light
- if @project.avatar_in_git
Project avatar in repository: #{ @project.avatar_in_git }
@@ -180,6 +181,7 @@
%ul
%li Build traces and artifacts
%li LFS objects
+ %li Container registry images
%hr
- if can? current_user, :archive_project, @project
.row.prepend-top-default
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index f135bf6f6b4..05a8475dcd6 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -22,14 +22,14 @@
%ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) }
%p.empty-message{ class: ('hidden' unless @prioritized_labels.empty?) } No prioritized labels yet
- if @prioritized_labels.present?
- = render partial: 'shared/label', collection: @prioritized_labels, as: :label
+ = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label
.other-labels
- if can?(current_user, :admin_label, @project)
%h5{ class: ('hide' if hide) } Other Labels
%ul.content-list.manage-labels-list.js-other-labels
- if @labels.present?
- = render partial: 'shared/label', collection: @labels, as: :label
+ = render partial: 'shared/label', subject: @project, collection: @labels, as: :label
= paginate @labels, theme: 'gitlab'
- if @labels.blank?
.nothing-here-block
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 6624d5cb427..4e41a15d9f4 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -33,7 +33,12 @@
.form-actions
- if @page && @page.persisted?
= f.submit 'Save changes', class: "btn-save btn"
- = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-cancel"
+ .pull-right
+ - if can?(current_user, :admin_wiki, @project)
+ = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-danger btn-grouped" do
+ Delete
+ = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-cancel btn-grouped"
- else
= f.submit 'Create page', class: "btn-create btn"
- = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, :home), class: "btn btn-cancel"
+ .pull-right
+ = link_to "Cancel", namespace_project_wiki_path(@project.namespace, @project, :home), class: "btn btn-cancel"
diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 4ea75dbbf0c..763c2fea39b 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -7,6 +7,3 @@
- if can?(current_user, :create_wiki, @project)
= link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn" do
Edit
- - if can?(current_user, :admin_wiki, @project)
- = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-remove" do
- Delete
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 233538bb488..679d6018bef 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -19,7 +19,5 @@
- if can?(current_user, :create_wiki, @project)
= link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
New Page
- = render 'main_links'
-
= render 'form'
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index 3480800369a..c367ae336db 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -1,6 +1,9 @@
%ul.nav-links.event-filter.scrolling-tabs
= event_filter_link EventFilter.all, 'All'
- = event_filter_link EventFilter.push, 'Push events'
- = event_filter_link EventFilter.merged, 'Merge events'
- = event_filter_link EventFilter.comments, 'Comments'
+ - if event_filter_visible(:repository)
+ = event_filter_link EventFilter.push, 'Push events'
+ - if event_filter_visible(:merge_requests)
+ = event_filter_link EventFilter.merged, 'Merge events'
+ - if event_filter_visible(:issues)
+ = event_filter_link EventFilter.comments, 'Comments'
= event_filter_link EventFilter.team, 'Team'
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 40c8d2af226..6ccdef0df46 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -1,6 +1,7 @@
- label_css_id = dom_id(label)
-- open_issues_count = label.open_issues_count(current_user, @project)
-- open_merge_requests_count = label.open_merge_requests_count(current_user, @project)
+- open_issues_count = label.open_issues_count(current_user)
+- open_merge_requests_count = label.open_merge_requests_count(current_user)
+- subject = local_assigns[:subject]
%li{id: label_css_id, data: { id: label.id } }
= render "shared/label_row", label: label
@@ -12,10 +13,10 @@
.dropdown-menu.dropdown-menu-align-right
%ul
%li
- = link_to_label(label, subject: @project, type: :merge_request) do
+ = link_to_label(label, subject: subject, type: :merge_request) do
= pluralize open_merge_requests_count, 'merge request'
%li
- = link_to_label(label, subject: @project) do
+ = link_to_label(label, subject: subject) do
= pluralize open_issues_count, 'open issue'
- if current_user
%li.label-subscription{ data: toggle_subscription_data(label) }
@@ -28,9 +29,9 @@
= link_to 'Delete', destroy_label_path(label), title: 'Delete', method: :delete, remote: true, data: {confirm: 'Remove this label? Are you sure?'}
.pull-right.hidden-xs.hidden-sm.hidden-md
- = link_to_label(label, subject: @project, type: :merge_request, css_class: 'btn btn-transparent btn-action') do
+ = link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action') do
= pluralize open_merge_requests_count, 'merge request'
- = link_to_label(label, subject: @project, css_class: 'btn btn-transparent btn-action') do
+ = link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action') do
= pluralize open_issues_count, 'open issue'
- if current_user
diff --git a/app/views/shared/empty_states/_todos_all_done.svg b/app/views/shared/empty_states/_todos_all_done.svg
new file mode 100644
index 00000000000..94b5c2e0ea0
--- /dev/null
+++ b/app/views/shared/empty_states/_todos_all_done.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 293 216"><g fill="none" fill-rule="evenodd"><g transform="rotate(-5 211.388 -693.89)"><rect width="163.6" height="200" x=".2" fill="#FFF" stroke="#EEE" stroke-width="3" stroke-linecap="round" stroke-dasharray="6 9" rx="6"/><g transform="translate(24 38)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#6B4FBB" opacity=".5" rx="1.5"/></g><g transform="translate(24 83)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g><g transform="translate(24 130)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g></g><path fill="#FFCE29" d="M30 11l-1.8 4-2-4-4-1.8 4-2 2-4 2 4 4 2M286 60l-2.7 6.3-3-6-6-3 6-3 3-6 2.8 6.2 6.6 2.8M263 97l-2 4-2-4-4-2 4-2 2-4 2 4 4 2M12 85l-2.7 6.3-3-6-6-3 6-3 3-6 2.8 6.2 6.6 2.8"/></g></svg>
diff --git a/app/views/shared/empty_states/_todos_empty.svg b/app/views/shared/empty_states/_todos_empty.svg
new file mode 100644
index 00000000000..b1e661268fb
--- /dev/null
+++ b/app/views/shared/empty_states/_todos_empty.svg
@@ -0,0 +1,110 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 284 337" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <rect id="a" width="180" height="220" x="66.2" y="74.4" rx="6"/>
+ <mask id="l" width="180" height="220" x="0" y="0" fill="#fff">
+ <use xlink:href="#a"/>
+ </mask>
+ <rect id="b" width="180" height="220" rx="6"/>
+ <mask id="m" width="180" height="220" x="0" y="0" fill="#fff">
+ <use xlink:href="#b"/>
+ </mask>
+ <rect id="c" width="28" height="28" rx="4"/>
+ <mask id="n" width="28" height="28" x="0" y="0" fill="#fff">
+ <use xlink:href="#c"/>
+ </mask>
+ <rect id="d" width="28" height="28" rx="4"/>
+ <mask id="o" width="28" height="28" x="0" y="0" fill="#fff">
+ <use xlink:href="#d"/>
+ </mask>
+ <circle id="e" cx="21.5" cy="21.5" r="21.5"/>
+ <mask id="p" width="43" height="43" x="0" y="0" fill="#fff">
+ <use xlink:href="#e"/>
+ </mask>
+ <circle id="f" cx="26.5" cy="26.5" r="26.5"/>
+ <mask id="q" width="53" height="53" x="0" y="0" fill="#fff">
+ <use xlink:href="#f"/>
+ </mask>
+ <circle id="g" cx="9.5" cy="4.5" r="4.5"/>
+ <mask id="r" width="13" height="13" x="-2" y="-2">
+ <path fill="#fff" d="M3-2h13v13H3z"/>
+ <use xlink:href="#g"/>
+ </mask>
+ <circle id="h" cx="26.5" cy="26.5" r="26.5"/>
+ <mask id="s" width="53" height="53" x="0" y="0" fill="#fff">
+ <use xlink:href="#h"/>
+ </mask>
+ <circle id="i" cx="21.5" cy="21.5" r="21.5"/>
+ <mask id="t" width="43" height="43" x="0" y="0" fill="#fff">
+ <use xlink:href="#i"/>
+ </mask>
+ <path id="j" d="M18 38h15c10.5 0 19-8.5 19-19S43.5 0 33 0H19C8.5 0 0 8.5 0 19c0 6.3 3 12 7.8 15.3l5.2 9c.6 1 1.4 1 2 0l3-5.3z"/>
+ <mask id="u" width="52" height="44" x="0" y="0" fill="#fff">
+ <use xlink:href="#j"/>
+ </mask>
+ <circle id="k" cx="18.5" cy="18.5" r="18.5"/>
+ <mask id="v" width="37" height="37" x="0" y="0" fill="#fff">
+ <use xlink:href="#k"/>
+ </mask>
+ </defs>
+ <g fill="none" fill-rule="evenodd" transform="translate(-6 -4)">
+ <use stroke="#EEE" stroke-width="6" mask="url(#l)" transform="rotate(-5 156.245 184.425)" xlink:href="#a"/>
+ <g transform="rotate(5 -707.333 618.042)">
+ <use fill="#FFF" stroke="#EEE" stroke-width="6" mask="url(#m)" xlink:href="#b"/>
+ <g transform="translate(29 24)">
+ <path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/>
+ <path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/>
+ <rect width="86" height="3" x="40" y="11" fill="#6B4FBB" opacity=".5" rx="1.5"/>
+ <rect width="43" height="3" x="40" y="21" fill="#6B4FBB" opacity=".5" rx="1.5"/>
+ </g>
+ <g transform="translate(29 69)">
+ <path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/>
+ <path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/>
+ <rect width="86" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/>
+ <rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/>
+ </g>
+ <g transform="translate(28 160)">
+ <use stroke="#E5E5E5" stroke-width="6" mask="url(#n)" opacity=".7" xlink:href="#c"/>
+ <rect width="26" height="3" x="41" y="7" fill="#ECECEC" rx="1.5"/>
+ <rect width="43" height="3" x="41" y="17" fill="#ECECEC" rx="1.5"/>
+ </g>
+ <g transform="translate(28 116)">
+ <use stroke="#E5E5E5" stroke-width="6" mask="url(#o)" xlink:href="#d"/>
+ <rect width="86" height="3" x="41" y="7" fill="#E5E5E5" rx="1.5"/>
+ <rect width="43" height="3" x="41" y="17" fill="#E5E5E5" rx="1.5"/>
+ </g>
+ </g>
+ <g transform="rotate(-15 601.917 -782.362)">
+ <use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#p)" xlink:href="#e"/>
+ <text fill="#6B4FBB" font-family="SourceSansPro-Black, Source Sans Pro" font-size="20" font-weight="700" letter-spacing="-.1">
+ <tspan x="12" y="27">@</tspan>
+ </text>
+ </g>
+ <g transform="rotate(15 -686.59 1035.907)">
+ <use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#q)" xlink:href="#f"/>
+ <path fill="#FC6D26" d="M26.5 38.2c3.3 0 9.5-2.5 9.5-9.6 0-7-2.4-6.6-9.5-6.6-7 0-9.5-.4-9.5 6.6s6.2 9.6 9.5 9.6z"/>
+ <g transform="translate(17 14)">
+ <use fill="#FC6D26" xlink:href="#g"/>
+ <use stroke="#FFF" stroke-width="4" mask="url(#r)" xlink:href="#g"/>
+ </g>
+ </g>
+ <g transform="rotate(15 -85.125 65.185)">
+ <use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#s)" xlink:href="#h"/>
+ <path fill="#6B4FBB" d="M24 18.5c0-1.4 1-2.5 2.5-2.5 1.4 0 2.5 1 2.5 2.5v9c0 1.4-1 2.5-2.5 2.5-1.4 0-2.5-1-2.5-2.5v-9zM26.5 37c1.4 0 2.5-1 2.5-2.5 0-1.4-1-2.5-2.5-2.5-1.4 0-2.5 1-2.5 2.5 0 1.4 1 2.5 2.5 2.5z"/>
+ </g>
+ <g transform="rotate(-15 716.492 78.873)">
+ <use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#t)" xlink:href="#i"/>
+ <path fill="#FC6D26" d="M20 23v-3h3v3h-3zm0 3v1.5c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5V26h-2.5c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5H17v-3h-1.5c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5H17v-2.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5V17h3v-1.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5V17h2.5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5H26v3h1.5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5H26v2.5c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5V26h-3z"/>
+ </g>
+ <g transform="rotate(-15 129.114 -585.74)">
+ <use stroke="#FDE5D8" stroke-width="6" mask="url(#u)" xlink:href="#j"/>
+ <circle cx="16" cy="20" r="2" fill="#FC6D26"/>
+ <circle cx="27" cy="20" r="2" fill="#FC6D26"/>
+ <circle cx="38" cy="20" r="2" fill="#FC6D26"/>
+ </g>
+ <g transform="rotate(-15 1254.8 -458.986)">
+ <use stroke="#FDE5D8" stroke-width="6" mask="url(#v)" xlink:href="#k"/>
+ <path fill="#FC6D26" d="M10.6 19l2-2c.5-.5.5-1 0-1.5-.3-.4-1-.4-1.3 0l-2.8 2.8c-.2.2-.3.4-.3.7 0 .3 0 .5.3.7l2.8 2.8c.4.4 1 .4 1.4 0 .4-.4.4-1 0-1.4l-2-2zm14.8 0l-2-2c-.5-.5-.5-1 0-1.5.3-.4 1-.4 1.3 0l2.8 2.8c.2.2.3.4.3.7 0 .3 0 .5-.3.7l-2.8 2.8c-.4.4-1 .4-1.4 0-.4-.4-.4-1 0-1.4l2-2z"/>
+ <rect width="2" height="7" x="17" y="15.1" fill="#FC6D26" opacity=".5" transform="rotate(15 18.002 18.64)" rx="1"/>
+ </g>
+ </g>
+</svg>
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index dc4ee3074d2..562291a61df 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -24,7 +24,8 @@
%span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
= visibility_level_icon(group.visibility_level, fw: false)
- = image_tag group_icon(group), class: "avatar s40 hidden-xs"
+ .image-container.s40
+ = image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title
= link_to group, class: 'group-name' do
= group.name
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
index f27a9002ec2..40fe53e6a8d 100644
--- a/app/views/shared/issuable/_milestone_dropdown.html.haml
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -1,10 +1,10 @@
- project = @target_project || @project
- extra_class = extra_class || ''
- show_menu_above = show_menu_above || false
-- selected_text = selected.try(:title)
+- selected_text = selected.try(:title) || params[:milestone_title]
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone")
- if selected.present?
- = hidden_field_tag(name, name == :milestone_title ? selected.title : selected.id)
+ = hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id)
= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone",
placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
- if project
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index e8668048703..3d2122a159c 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -32,10 +32,11 @@
= link_to project_path(project), class: dom_class(project) do
- if avatar
.dash-project-avatar
- - if use_creator_avatar
- = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
- - else
- = project_icon(project, alt: '', class: 'avatar project-avatar s40')
+ .image-container.s40
+ - if use_creator_avatar
+ = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
+ - else
+ = project_icon(project, alt: '', class: 'avatar project-avatar s40')
%span.project-full-name
%span.namespace-name
- if project.namespace && !skip_namespace
diff --git a/app/views/users/_groups.html.haml b/app/views/users/_groups.html.haml
index f360fbb3d5d..78f253f9023 100644
--- a/app/views/users/_groups.html.haml
+++ b/app/views/users/_groups.html.haml
@@ -1,4 +1,5 @@
.clearfix
- groups.each do |group|
= link_to group, class: 'profile-groups-avatars inline', title: group.name do
- = image_tag group_icon(group), class: 'avatar group-avatar s40'
+ .image-container.s40
+ = image_tag group_icon(group), class: 'avatar group-avatar s40'
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index eee0ca12af9..2fff6b0105d 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -16,7 +16,7 @@ class PostReceive
post_received = Gitlab::GitPostReceive.new(repo_path, identifier, changes)
if post_received.project.nil?
- log("Triggered hook for non-existing project with full path \"#{repo_path} \"")
+ log("Triggered hook for non-existing project with full path \"#{repo_path}\"")
return false
end
@@ -25,7 +25,7 @@ class PostReceive
elsif post_received.regular_project?
process_project_changes(post_received)
else
- log("Triggered hook for unidentifiable repository type with full path \"#{repo_path} \"")
+ log("Triggered hook for unidentifiable repository type with full path \"#{repo_path}\"")
false
end
end
@@ -37,7 +37,7 @@ class PostReceive
@user ||= post_received.identify(newrev)
unless @user
- log("Triggered hook for non-existing user \"#{post_received.identifier} \"")
+ log("Triggered hook for non-existing user \"#{post_received.identifier}\"")
return false
end
diff --git a/bin/changelog b/bin/changelog
new file mode 100755
index 00000000000..a0d1ad2d730
--- /dev/null
+++ b/bin/changelog
@@ -0,0 +1,164 @@
+#!/usr/bin/env ruby
+#
+# Generate a changelog entry file in the correct location.
+#
+# Automatically stages the file and amends the previous commit if the `--amend`
+# argument is used.
+
+require 'optparse'
+require 'yaml'
+
+Options = Struct.new(
+ :amend,
+ :author,
+ :dry_run,
+ :merge_request,
+ :title
+)
+
+class ChangelogOptionParser
+ def self.parse(argv)
+ options = Options.new
+
+ parser = OptionParser.new do |opts|
+ opts.banner = "Usage: #{__FILE__} [options]"
+
+ # Note: We do not provide a shorthand for this in order to match the `git
+ # commit` interface
+ opts.on('--amend', 'Amend the previous commit') do |value|
+ options.amend = value
+ end
+
+ opts.on('-m', '--merge-request [integer]', Integer, 'Merge Request ID') do |value|
+ options.merge_request = value
+ end
+
+ opts.on('-n', '--dry-run', "Don't actually write anything, just print") do |value|
+ options.dry_run = value
+ end
+
+ opts.on('-u', '--git-username', 'Use Git user.name configuration as the author') do |value|
+ options.author = git_user_name if value
+ end
+
+ opts.on('-h', '--help', 'Print help message') do
+ $stdout.puts opts
+ exit
+ end
+ end
+
+ parser.parse!(argv)
+
+ # Title is everything that remains, but let's clean it up a bit
+ options.title = argv.join(' ').strip.squeeze(' ').tr("\r\n", '')
+
+ options
+ end
+
+ def self.git_user_name
+ %x{git config user.name}.strip
+ end
+end
+
+class ChangelogEntry
+ attr_reader :options
+
+ def initialize(options)
+ @options = options
+
+ assert_feature_branch!
+ assert_new_file!
+ assert_title!
+
+ $stdout.puts "\e[32mcreate\e[0m #{file_path}"
+ $stdout.puts contents
+
+ unless options.dry_run
+ write
+ amend_commit if options.amend
+ end
+ end
+
+ def contents
+ YAML.dump(
+ 'title' => title,
+ 'merge_request' => options.merge_request,
+ 'author' => options.author
+ )
+ end
+
+ def write
+ File.write(file_path, contents)
+ end
+
+ def amend_commit
+ %x{git add #{file_path}}
+ exec("git commit --amend")
+ end
+
+ private
+
+ def fail_with(message)
+ $stderr.puts "\e[31merror\e[0m #{message}"
+ exit 1
+ end
+
+ def assert_feature_branch!
+ return unless branch_name == 'master'
+
+ fail_with "Create a branch first!"
+ end
+
+ def assert_new_file!
+ return unless File.exist?(file_path)
+
+ fail_with "#{file_path} already exists!"
+ end
+
+ def assert_title!
+ return if options.title.length > 0 || options.amend
+
+ fail_with "Provide a title for the changelog entry or use `--amend`" \
+ " to use the title from the previous commit."
+ end
+
+ def title
+ if options.title.empty?
+ last_commit_subject
+ else
+ options.title
+ end
+ end
+
+ def last_commit_subject
+ %x{git log --format="%s" -1}.strip
+ end
+
+ def file_path
+ File.join(
+ unreleased_path,
+ branch_name.gsub(/[^\w-]/, '-') << '.yml'
+ )
+ end
+
+ def unreleased_path
+ File.join('changelogs', 'unreleased').tap do |path|
+ path << '-ee' if ee?
+ end
+ end
+
+ def ee?
+ @ee ||= File.exist?(File.expand_path('../CHANGELOG-EE.md', __dir__))
+ end
+
+ def branch_name
+ @branch_name ||= %x{git symbolic-ref HEAD}.strip.sub(%r{\Arefs/heads/}, '')
+ end
+end
+
+if $0 == __FILE__
+ options = ChangelogOptionParser.parse(ARGV)
+ ChangelogEntry.new(options)
+end
+
+# vim: ft=ruby
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index 74325872b09..c11296975b7 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -101,6 +101,13 @@
:why: GPL-licensed libraries cannot be linked to from non-GPL projects.
:versions: []
:when: 2016-05-02 05:29:43.904715000 Z
+- - :blacklist
+ - OSL-3.0
+ - :who: Sean McGivern
+ :why: The OSL license is a copyleft license
+ :versions: []
+ :when: 2016-10-28 11:02:15.540105000 Z
+
# GEM LICENSES
- - :license
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 3451b68cea5..699ab6075b6 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -432,7 +432,9 @@ production: &base
## Repositories settings
repositories:
# Paths where repositories can be stored. Give the canonicalized absolute pathname.
- # NOTE: REPOS PATHS MUST NOT CONTAIN ANY SYMLINK!!!
+ # IMPORTANT: None of the path components may be symlink, because
+ # gitlab-shell invokes Dir.pwd inside the repository path and that results
+ # real path not the symlink.
storages: # You must have at least a `default` storage path.
default: /home/git/repositories/
diff --git a/config/initializers/0_post_deployment_migrations.rb b/config/initializers/0_post_deployment_migrations.rb
new file mode 100644
index 00000000000..0068a03d214
--- /dev/null
+++ b/config/initializers/0_post_deployment_migrations.rb
@@ -0,0 +1,12 @@
+# Post deployment migrations are included by default. This file must be loaded
+# before other initializers as Rails may otherwise memoize a list of migrations
+# excluding the post deployment migrations.
+unless ENV['SKIP_POST_DEPLOYMENT_MIGRATIONS']
+ path = Rails.root.join('db', 'post_migrate').to_s
+
+ Rails.application.config.paths['db/migrate'] << path
+
+ # Rails memoizes migrations at certain points where it won't read the above
+ # path just yet. As such we must also update the following list of paths.
+ ActiveRecord::Migrator.migrations_paths << path
+end
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index f7e714cd6bc..0455a98dbfe 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -42,3 +42,19 @@ end
Sidekiq.configure_client do |config|
config.redis = redis_config_hash
end
+
+# The Sidekiq client API always adds the queue to the Sidekiq queue
+# list, but mail_room and gitlab-shell do not. This is only necessary
+# for monitoring.
+config = YAML.load_file(Rails.root.join('config', 'sidekiq_queues.yml').to_s)
+
+begin
+ Sidekiq.redis do |conn|
+ conn.pipelined do
+ config[:queues].each do |queue|
+ conn.sadd('queues', queue[0])
+ end
+ end
+ end
+rescue Redis::BaseError, SocketError
+end
diff --git a/db/post_migrate/.gitkeep b/db/post_migrate/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/db/post_migrate/.gitkeep
diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md
new file mode 100644
index 00000000000..f3c2e72341f
--- /dev/null
+++ b/doc/administration/raketasks/maintenance.md
@@ -0,0 +1,220 @@
+# Maintenance Rake Tasks
+
+## Gather information about GitLab and the system it runs on
+
+This command gathers information about your GitLab installation and the System it runs on. These may be useful when asking for help or reporting issues.
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake gitlab:env:info
+```
+
+**Source Installation**
+
+```
+bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+Example output:
+
+```
+System information
+System: Debian 7.8
+Current User: git
+Using RVM: no
+Ruby Version: 2.1.5p273
+Gem Version: 2.4.3
+Bundler Version: 1.7.6
+Rake Version: 10.3.2
+Sidekiq Version: 2.17.8
+
+GitLab information
+Version: 7.7.1
+Revision: 41ab9e1
+Directory: /home/git/gitlab
+DB Adapter: postgresql
+URL: https://gitlab.example.com
+HTTP Clone URL: https://gitlab.example.com/some-project.git
+SSH Clone URL: git@gitlab.example.com:some-project.git
+Using LDAP: no
+Using Omniauth: no
+
+GitLab Shell
+Version: 2.4.1
+Repositories: /home/git/repositories/
+Hooks: /home/git/gitlab-shell/hooks/
+Git: /usr/bin/git
+```
+
+## Check GitLab configuration
+
+Runs the following rake tasks:
+
+- `gitlab:gitlab_shell:check`
+- `gitlab:sidekiq:check`
+- `gitlab:app:check`
+
+It will check that each component was setup according to the installation guide and suggest fixes for issues found.
+
+You may also have a look at our [Trouble Shooting Guide](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Trouble-Shooting-Guide).
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake gitlab:check
+```
+
+**Source Installation**
+
+```
+bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+NOTE: Use SANITIZE=true for gitlab:check if you want to omit project names from the output.
+
+Example output:
+
+```
+Checking Environment ...
+
+Git configured for git user? ... yes
+Has python2? ... yes
+python2 is supported version? ... yes
+
+Checking Environment ... Finished
+
+Checking GitLab Shell ...
+
+GitLab Shell version? ... OK (1.2.0)
+Repo base directory exists? ... yes
+Repo base directory is a symlink? ... no
+Repo base owned by git:git? ... yes
+Repo base access is drwxrws---? ... yes
+post-receive hook up-to-date? ... yes
+post-receive hooks in repos are links: ... yes
+
+Checking GitLab Shell ... Finished
+
+Checking Sidekiq ...
+
+Running? ... yes
+
+Checking Sidekiq ... Finished
+
+Checking GitLab ...
+
+Database config exists? ... yes
+Database is SQLite ... no
+All migrations up? ... yes
+GitLab config exists? ... yes
+GitLab config outdated? ... no
+Log directory writable? ... yes
+Tmp directory writable? ... yes
+Init script exists? ... yes
+Init script up-to-date? ... yes
+Redis version >= 2.0.0? ... yes
+
+Checking GitLab ... Finished
+```
+
+## Rebuild authorized_keys file
+
+In some case it is necessary to rebuild the `authorized_keys` file.
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake gitlab:shell:setup
+```
+
+**Source Installation**
+
+```
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:shell:setup RAILS_ENV=production
+```
+
+```
+This will rebuild an authorized_keys file.
+You will lose any data stored in authorized_keys file.
+Do you want to continue (yes/no)? yes
+```
+
+## Clear redis cache
+
+If for some reason the dashboard shows wrong information you might want to
+clear Redis' cache.
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake cache:clear
+```
+
+**Source Installation**
+
+```
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
+```
+
+## Precompile the assets
+
+Sometimes during version upgrades you might end up with some wrong CSS or
+missing some icons. In that case, try to precompile the assets again.
+
+Note that this only applies to source installations and does NOT apply to
+Omnibus packages.
+
+**Source Installation**
+
+```
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
+```
+
+For omnibus versions, the unoptimized assets (JavaScript, CSS) are frozen at
+the release of upstream GitLab. The omnibus version includes optimized versions
+of those assets. Unless you are modifying the JavaScript / CSS code on your
+production machine after installing the package, there should be no reason to redo
+rake assets:precompile on the production machine. If you suspect that assets
+have been corrupted, you should reinstall the omnibus package.
+
+## Tracking Deployments
+
+GitLab provides a Rake task that lets you track deployments in GitLab
+Performance Monitoring. This Rake task simply stores the current GitLab version
+in the GitLab Performance Monitoring database.
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake gitlab:track_deployment
+```
+
+**Source Installation**
+
+```
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:track_deployment RAILS_ENV=production
+```
+
+## Create or repair repository hooks symlink
+
+If the GitLab shell hooks directory location changes or another circumstance
+leads to the hooks symlink becoming missing or invalid, run this Rake task
+to create or repair the symlinks.
+
+**Omnibus Installation**
+
+```
+sudo gitlab-rake gitlab:shell:create_hooks
+```
+
+**Source Installation**
+
+```
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:shell:create_hooks RAILS_ENV=production
+```
diff --git a/doc/administration/troubleshooting/debug.md b/doc/administration/troubleshooting/debug.md
index d8dce4388e1..6f1356ddf8f 100644
--- a/doc/administration/troubleshooting/debug.md
+++ b/doc/administration/troubleshooting/debug.md
@@ -107,7 +107,7 @@ downtime. Otherwise skip to the next section.
1. To see the current threads, run:
```
- apply all thread bt
+ thread apply all bt
```
1. Once you're done debugging with `gdb`, be sure to detach from the process and exit:
diff --git a/doc/api/README.md b/doc/api/README.md
index 3fbe5197a21..f65b934b9db 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -23,6 +23,7 @@ following locations:
- [Group Access Requests](access_requests.md)
- [Group Members](members.md)
- [Issues](issues.md)
+- [Issue Boards](boards.md)
- [Keys](keys.md)
- [Labels](labels.md)
- [Merge Requests](merge_requests.md)
diff --git a/doc/api/projects.md b/doc/api/projects.md
index b69db90e70d..4f4b20a1874 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -598,7 +598,7 @@ Parameters:
| `container_registry_enabled` | boolean | no | Enable container registry for this project |
| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
-| `visibility_level` | integer | no | See [project visibility level][#project-visibility-level] |
+| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) |
| `import_url` | string | no | URL to import repository from |
| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds |
@@ -630,7 +630,7 @@ Parameters:
| `container_registry_enabled` | boolean | no | Enable container registry for this project |
| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
-| `visibility_level` | integer | no | See [project visibility level][#project-visibility-level] |
+| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) |
| `import_url` | string | no | URL to import repository from |
| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds |
@@ -661,7 +661,7 @@ Parameters:
| `container_registry_enabled` | boolean | no | Enable container registry for this project |
| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
-| `visibility_level` | integer | no | See [project visibility level][#project-visibility-level] |
+| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) |
| `import_url` | string | no | URL to import repository from |
| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds |
@@ -1139,6 +1139,7 @@ Parameters:
| `pipeline_events` | boolean | no | Trigger hook on pipeline events |
| `wiki_events` | boolean | no | Trigger hook on wiki events |
| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
+| `token` | string | no | Secret token to validate received payloads; this will not be returned in the response |
### Edit project hook
@@ -1164,6 +1165,7 @@ Parameters:
| `pipeline_events` | boolean | no | Trigger hook on pipeline events |
| `wiki_events` | boolean | no | Trigger hook on wiki events |
| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
+| `token` | string | no | Secret token to validate received payloads; this will not be returned in the response |
### Delete project hook
diff --git a/doc/api/tags.md b/doc/api/tags.md
index 54059117456..398b080e3f6 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -124,7 +124,7 @@ Parameters:
The message will be `nil` when creating a lightweight tag otherwise
it will contain the annotation.
-It returns 200 if the operation succeed. In case of an error,
+It returns 201 if the operation succeed. In case of an error,
405 with an explaining error message is returned.
## Delete a tag
diff --git a/doc/development/README.md b/doc/development/README.md
index 14d6f08e43a..bf1f054b7d5 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -21,6 +21,7 @@
## Process
+- [Generate a changelog entry with `bin/changelog`](changelog.md)
- [Code review guidelines](code_review.md) for reviewing code and having code reviewed.
- [Merge request performance guidelines](merge_request_performance_guidelines.md)
for ensuring merge requests do not negatively impact GitLab performance
@@ -41,6 +42,7 @@
- [What requires downtime?](what_requires_downtime.md)
- [Adding database indexes](adding_database_indexes.md)
+- [Post Deployment Migrations](post_deployment_migrations.md)
## Compliance
diff --git a/doc/development/changelog.md b/doc/development/changelog.md
new file mode 100644
index 00000000000..d08c476e9d6
--- /dev/null
+++ b/doc/development/changelog.md
@@ -0,0 +1,164 @@
+# Generate a changelog entry
+
+This guide contains instructions for generating a changelog entry data file, as
+well as information and history about our changelog process.
+
+## Overview
+
+Each bullet point, or **entry**, in our [`CHANGELOG.md`][changelog.md] file is
+generated from a single data file in the [`changelogs/unreleased/`][unreleased]
+(or corresponding EE) folder. The file is expected to be a [YAML] file in the
+following format:
+
+```yaml
+---
+title: "Going through change[log]s"
+merge_request: 1972
+author: Ozzy Osbourne
+```
+
+The `merge_request` value is a reference to a merge request that adds this
+entry, and the `author` key is used to give attribution to community
+contributors. Both are optional.
+
+If you're working on the GitLab EE repository, the entry will be added to
+`changelogs/unreleased-ee/` instead.
+
+[changelog.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG.md
+[unreleased]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/changelogs/
+[YAML]: https://en.wikipedia.org/wiki/YAML
+
+## Instructions
+
+A `bin/changelog` script is available to generate the changelog entry file
+automatically.
+
+Its simplest usage is to provide the value for `title`:
+
+```text
+$ bin/changelog 'Hey DZ, I added a feature to GitLab!'
+create changelogs/unreleased/my-feature.yml
+---
+title: Hey DZ, I added a feature to GitLab!
+merge_request:
+author:
+```
+
+The entry filename is based on the name of the current Git branch. If you run
+the command above on a branch called `feature/hey-dz`, it will generate a
+`changelogs/unreleased/feature-hey-dz` file.
+
+### Arguments
+
+| Argument | Shorthand | Purpose |
+| ----------------- | --------- | --------------------------------------------- |
+| `--amend` | | Amend the previous commit |
+| `--merge-request` | `-m` | Merge Request ID |
+| `--dry-run` | `-n` | Don't actually write anything, just print |
+| `--git-username` | `-u` | Use Git user.name configuration as the author |
+| `--help` | `-h` | Print help message |
+
+#### `--amend`
+
+You can pass the **`--amend`** argument to automatically stage the generated
+file and amend it to the previous commit.
+
+If you use **`--amend`** and don't provide a title, it will automatically use
+the "subject" of the previous commit, which is the first line of the commit
+message:
+
+```text
+$ git show --oneline
+ab88683 Added an awesome new feature to GitLab
+
+$ bin/changelog --amend
+create changelogs/unreleased/feature-hey-dz.yml
+---
+title: Added an awesome new feature to GitLab
+merge_request:
+author:
+```
+
+#### `--merge-request` or `-m`
+
+Use the **`--merge-request`** or **`-m`** argument to provide the
+`merge_request` value:
+
+```text
+$ bin/changelog 'Hey DZ, I added a feature to GitLab!' -m 1983
+create changelogs/unreleased/feature-hey-dz.yml
+---
+title: Hey DZ, I added a feature to GitLab!
+merge_request: 1983
+author:
+```
+
+#### `--dry-run` or `-n`
+
+Use the **`--dry-run`** or **`-n`** argument to prevent actually writing or
+committing anything:
+
+```text
+$ bin/changelog --amend --dry-run
+create changelogs/unreleased/feature-hey-dz.yml
+---
+title: Added an awesome new feature to GitLab
+merge_request:
+author:
+
+$ ls changelogs/unreleased/
+```
+
+#### `--git-username` or `-u`
+
+Use the **`--git-username`** or **`-u`** argument to automatically fill in the
+`author` value with your configured Git `user.name` value:
+
+```text
+$ git config user.name
+Jane Doe
+
+$ bin/changelog --u 'Hey DZ, I added a feature to GitLab!'
+create changelogs/unreleased/feature-hey-dz.yml
+---
+title: Hey DZ, I added a feature to GitLab!
+merge_request:
+author: Jane Doe
+```
+
+## History and Reasoning
+
+Our `CHANGELOG` file was previously updated manually by each contributor that
+felt their change warranted an entry. When two merge requests added their own
+entries at the same spot in the list, it created a merge conflict in one as soon
+as the other was merged. When we had dozens of merge requests fighting for the
+same changelog entry location, this quickly became a major source of merge
+conflicts and delays in development.
+
+This led us to a [boring solution] of "add your entry in a random location in
+the list." This actually worked pretty well as we got further along in each
+monthly release cycle, but at the start of a new cycle, when a new version
+section was added and there were fewer places to "randomly" add an entry, the
+conflicts became a problem again until we had a sufficient number of entries.
+
+On top of all this, it created an entirely different headache for [release managers]
+when they cherry-picked a commit into a stable branch for a patch release. If
+the commit included an entry in the `CHANGELOG`, it would include the entire
+changelog for the latest version in `master`, so the release manager would have
+to manually remove the later entries. They often would have had to do this
+multiple times per patch release. This was compounded when we had to release
+multiple patches at once due to a security issue.
+
+We needed to automate all of this manual work. So we [started brainstorming].
+After much discussion we settled on the current solution of one file per entry,
+and then compiling the entries into the overall `CHANGELOG.md` file during the
+[release process].
+
+[boring solution]: https://about.gitlab.com/handbook/#boring-solutions
+[release managers]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/release-manager.md
+[started brainstorming]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17826
+[release process]: https://gitlab.com/gitlab-org/release-tools
+
+---
+
+[Return to Development documentation](README.md)
diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md
index 159d5ce286d..b25ce79e89f 100644
--- a/doc/development/gotchas.md
+++ b/doc/development/gotchas.md
@@ -41,9 +41,9 @@ Rubocop](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-4-stable/.rubocop.yml#L9
[Exception]: http://stackoverflow.com/q/10048173/223897
-## Don't use inline CoffeeScript/JavaScript in views
+## Don't use inline JavaScript in views
-Using the inline `:coffee` or `:coffeescript` Haml filters comes with a
+Using the inline `:javascript` Haml filters comes with a
performance overhead. Using inline JavaScript is not a good way to structure your code and should be avoided.
_**Note:** We've [removed these two filters](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/initializers/hamlit.rb)
@@ -51,9 +51,7 @@ in an initializer._
### Further reading
-- Pull Request: [Replace CoffeeScript block into JavaScript in Views](https://git.io/vztMu)
- Stack Overflow: [Why you should not write inline JavaScript](http://programmers.stackexchange.com/questions/86589/why-should-i-avoid-inline-scripting)
-- Stack Overflow: [Performance implications of using :coffescript filter inside HAML templates?](http://stackoverflow.com/a/17571242/223897)
## ID-based CSS selectors need to be a bit more specific
diff --git a/doc/development/licensing.md b/doc/development/licensing.md
index 05972b33fdb..5d177eb26ee 100644
--- a/doc/development/licensing.md
+++ b/doc/development/licensing.md
@@ -62,6 +62,7 @@ Libraries with the following licenses are unacceptable for use:
- [GNU GPL][GPL] (version 1, [version 2][GPLv2], [version 3][GPLv3], or any future versions): GPL-licensed libraries cannot be linked to from non-GPL projects.
- [GNU AGPLv3][AGPLv3]: AGPL-licensed libraries cannot be linked to from non-GPL projects.
+- [Open Software License (OSL)][OSL]: is a copyleft license. In addition, the FSF [recommend against its use][OSL-GNU].
## Notes
@@ -93,3 +94,5 @@ Gems which are included only in the "development" or "test" groups by Bundler ar
[AGPLv3]: http://choosealicense.com/licenses/agpl-3.0/
[GNU-GPL-FAQ]: http://www.gnu.org/licenses/gpl-faq.html#IfLibraryIsGPL
[OSI-GPL]: https://opensource.org/faq#linking-proprietary-code
+[OSL]: https://opensource.org/licenses/OSL-3.0
+[OSL-GNU]: https://www.gnu.org/licenses/license-list.en.html#OSL
diff --git a/doc/development/post_deployment_migrations.md b/doc/development/post_deployment_migrations.md
new file mode 100644
index 00000000000..cfc91539bee
--- /dev/null
+++ b/doc/development/post_deployment_migrations.md
@@ -0,0 +1,75 @@
+# Post Deployment Migrations
+
+Post deployment migrations are regular Rails migrations that can optionally be
+executed after a deployment. By default these migrations are executed alongside
+the other migrations. To skip these migrations you will have to set the
+environment variable `SKIP_POST_DEPLOYMENT_MIGRATIONS` to a non-empty value
+when running `rake db:migrate`.
+
+For example, this would run all migrations including any post deployment
+migrations:
+
+```bash
+bundle exec rake db:migrate
+```
+
+This however will skip post deployment migrations:
+
+```bash
+SKIP_POST_DEPLOYMENT_MIGRATIONS=true bundle exec rake db:migrate
+```
+
+## Deployment Integration
+
+Say you're using Chef for deploying new versions of GitLab and you'd like to run
+post deployment migrations after deploying a new version. Let's assume you
+normally use the command `chef-client` to do so. To make use of this feature
+you'd have to run this command as follows:
+
+```bash
+SKIP_POST_DEPLOYMENT_MIGRATIONS=true sudo chef-client
+```
+
+Once all servers have been updated you can run `chef-client` again on a single
+server _without_ the environment variable.
+
+The process is similar for other deployment techniques: first you would deploy
+with the environment variable set, then you'll essentially re-deploy a single
+server but with the variable _unset_.
+
+## Creating Migrations
+
+To create a post deployment migration you can use the following Rails generator:
+
+```bash
+bundle exec rails g post_deployment_migration migration_name_here
+```
+
+This will generate the migration file in `db/post_migrate`. These migrations
+behave exactly like regular Rails migrations.
+
+## Use Cases
+
+Post deployment migrations can be used to perform migrations that mutate state
+that an existing version of GitLab depends on. For example, say you want to
+remove a column from a table. This requires downtime as a GitLab instance
+depends on this column being present while it's running. Normally you'd follow
+these steps in such a case:
+
+1. Stop the GitLab instance
+2. Run the migration removing the column
+3. Start the GitLab instance again
+
+Using post deployment migrations we can instead follow these steps:
+
+1. Deploy a new version of GitLab while ignoring post deployment migrations
+2. Re-run `rake db:migrate` but without the environment variable set
+
+Here we don't need any downtime as the migration takes place _after_ a new
+version (which doesn't depend on the column anymore) has been deployed.
+
+Some other examples where these migrations are useful:
+
+* Cleaning up data generated due to a bug in GitLab
+* Removing tables
+* Migrating jobs from one Sidekiq queue to another
diff --git a/doc/development/testing.md b/doc/development/testing.md
index 513457d203a..b0b26ccf57a 100644
--- a/doc/development/testing.md
+++ b/doc/development/testing.md
@@ -36,8 +36,8 @@ the command line via `bundle exec teaspoon`, or via a web browser at
`http://localhost:3000/teaspoon` when the Rails server is running.
- JavaScript tests live in `spec/javascripts/`, matching the folder structure of
- `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.coffee` has a corresponding
- `spec/javascripts/behaviors/autosize_spec.js.coffee` file.
+ `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.es6` has a corresponding
+ `spec/javascripts/behaviors/autosize_spec.js.es6` file.
- Haml fixtures required for JavaScript tests live in
`spec/javascripts/fixtures`. They should contain the bare minimum amount of
markup necessary for the test.
@@ -132,6 +132,42 @@ Adding new Spinach scenarios is acceptable _only if_ the new scenario requires
no more than one new `step` definition. If more than that is required, the
test should be re-implemented using RSpec instead.
+## Testing Rake Tasks
+
+To make testing Rake tasks a little easier, there is a helper that can be included
+in lieu of the standard Spec helper. Instead of `require 'spec_helper'`, use
+`require 'rake_helper'`. The helper includes `spec_helper` for you, and configures
+a few other things to make testing Rake tasks easier.
+
+At a minimum, requiring the Rake helper will redirect `stdout`, include the
+runtime task helpers, and include the `RakeHelpers` Spec support module.
+
+The `RakeHelpers` module exposes a `run_rake_task(<task>)` method to make
+executing tasks simple. See `spec/support/rake_helpers.rb` for all available
+methods.
+
+Example:
+
+```ruby
+require 'rake_helper'
+
+describe 'gitlab:shell rake tasks' do
+ before do
+ Rake.application.rake_require 'tasks/gitlab/shell'
+
+ stub_warn_user_is_not_gitlab
+ end
+
+ describe 'install task' do
+ it 'invokes create_hooks task' do
+ expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke)
+
+ run_rake_task('gitlab:shell:install')
+ end
+ end
+end
+```
+
---
[Return to Development documentation](README.md)
diff --git a/doc/integration/jira.md b/doc/integration/jira.md
index cf1557ddc44..2e31fd994de 100644
--- a/doc/integration/jira.md
+++ b/doc/integration/jira.md
@@ -135,7 +135,7 @@ password as they will be needed when configuring GitLab in the next section.
JIRA configuration in GitLab is done via a project's **Services**.
-#### GitLab 13.0 with JIRA v1000.x
+#### GitLab 8.13.0 with JIRA v1000.x
To enable JIRA integration in a project, navigate to the project's
and open the context menu clicking on the top right gear icon, then go to
@@ -160,7 +160,7 @@ with the linked JIRA project.
#### GitLab 6.x-7.7 with JIRA v6.x
-_**Note:** GitLab versions 13.0 and up contain various integration improvements.
+_**Note:** GitLab versions 8.13.0 and up contain various integration improvements.
We strongly recommend upgrading._
In `gitlab.yml` enable the JIRA issue tracker section by
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index f3b2a288776..4a242c321aa 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -268,13 +268,20 @@ message `Can't verify CSRF token authenticity`. This means that there is an erro
the SAML request, but this error never reaches GitLab due to the CSRF check.
To bypass this you can add `skip_before_action :verify_authenticity_token` to the
-`omniauth_callbacks_controller.rb` file. This will allow the error to hit GitLab,
-where it can then be seen in the usual logs, or as a flash message in the login
-screen.
-
-That file is located at `/opt/gitlab/embedded/service/gitlab-rails/app/controllers`
-for Omnibus installations and by default on `/home/git/gitlab/app/controllers` for
-installations from source.
+`omniauth_callbacks_controller.rb` file immediately after the `class` line and
+comment out the `protect_from_forgery` line using a `#` then restart Unicorn. This
+will allow the error to hit GitLab, where it can then be seen in the usual logs,
+or as a flash message on the login screen.
+
+That file is located in `/opt/gitlab/embedded/service/gitlab-rails/app/controllers`
+for Omnibus installations and by default in `/home/git/gitlab/app/controllers` for
+installations from source. Restart Unicorn using the `sudo gitlab-ctl restart unicorn`
+command on Omnibus installations and `sudo service gitlab restart` on installations
+from source.
+
+You may also find the [SSO Tracer](https://addons.mozilla.org/en-US/firefox/addon/sso-tracer)
+(Firefox) and [SAML Chrome Panel](https://chrome.google.com/webstore/detail/saml-chrome-panel/paijfdbeoenhembfhkhllainmocckace)
+(Chrome) browser extensions useful in your debugging.
### Invalid audience
diff --git a/doc/raketasks/maintenance.md b/doc/raketasks/maintenance.md
index 315cb56a089..266aeb7d60e 100644
--- a/doc/raketasks/maintenance.md
+++ b/doc/raketasks/maintenance.md
@@ -1,188 +1,3 @@
-# Maintenance
+# Maintenance Rake Tasks
-## Gather information about GitLab and the system it runs on
-
-This command gathers information about your GitLab installation and the System it runs on. These may be useful when asking for help or reporting issues.
-
-```
-# omnibus-gitlab
-sudo gitlab-rake gitlab:env:info
-
-# installation from source
-bundle exec rake gitlab:env:info RAILS_ENV=production
-```
-
-Example output:
-
-```
-System information
-System: Debian 7.8
-Current User: git
-Using RVM: no
-Ruby Version: 2.1.5p273
-Gem Version: 2.4.3
-Bundler Version: 1.7.6
-Rake Version: 10.3.2
-Sidekiq Version: 2.17.8
-
-GitLab information
-Version: 7.7.1
-Revision: 41ab9e1
-Directory: /home/git/gitlab
-DB Adapter: postgresql
-URL: https://gitlab.example.com
-HTTP Clone URL: https://gitlab.example.com/some-project.git
-SSH Clone URL: git@gitlab.example.com:some-project.git
-Using LDAP: no
-Using Omniauth: no
-
-GitLab Shell
-Version: 2.4.1
-Repositories: /home/git/repositories/
-Hooks: /home/git/gitlab-shell/hooks/
-Git: /usr/bin/git
-```
-
-## Check GitLab configuration
-
-Runs the following rake tasks:
-
-- `gitlab:gitlab_shell:check`
-- `gitlab:sidekiq:check`
-- `gitlab:app:check`
-
-It will check that each component was setup according to the installation guide and suggest fixes for issues found.
-
-You may also have a look at our [Trouble Shooting Guide](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Trouble-Shooting-Guide).
-
-```
-# omnibus-gitlab
-sudo gitlab-rake gitlab:check
-
-# installation from source
-bundle exec rake gitlab:check RAILS_ENV=production
-```
-
-NOTE: Use SANITIZE=true for gitlab:check if you want to omit project names from the output.
-
-Example output:
-
-```
-Checking Environment ...
-
-Git configured for git user? ... yes
-Has python2? ... yes
-python2 is supported version? ... yes
-
-Checking Environment ... Finished
-
-Checking GitLab Shell ...
-
-GitLab Shell version? ... OK (1.2.0)
-Repo base directory exists? ... yes
-Repo base directory is a symlink? ... no
-Repo base owned by git:git? ... yes
-Repo base access is drwxrws---? ... yes
-post-receive hook up-to-date? ... yes
-post-receive hooks in repos are links: ... yes
-
-Checking GitLab Shell ... Finished
-
-Checking Sidekiq ...
-
-Running? ... yes
-
-Checking Sidekiq ... Finished
-
-Checking GitLab ...
-
-Database config exists? ... yes
-Database is SQLite ... no
-All migrations up? ... yes
-GitLab config exists? ... yes
-GitLab config outdated? ... no
-Log directory writable? ... yes
-Tmp directory writable? ... yes
-Init script exists? ... yes
-Init script up-to-date? ... yes
-Redis version >= 2.0.0? ... yes
-
-Checking GitLab ... Finished
-```
-
-## Rebuild authorized_keys file
-
-In some case it is necessary to rebuild the `authorized_keys` file.
-
-For Omnibus-packages:
-```
-sudo gitlab-rake gitlab:shell:setup
-```
-
-For installations from source:
-```
-cd /home/git/gitlab
-sudo -u git -H bundle exec rake gitlab:shell:setup RAILS_ENV=production
-```
-
-```
-This will rebuild an authorized_keys file.
-You will lose any data stored in authorized_keys file.
-Do you want to continue (yes/no)? yes
-```
-
-## Clear redis cache
-
-If for some reason the dashboard shows wrong information you might want to
-clear Redis' cache.
-
-For Omnibus-packages:
-```
-sudo gitlab-rake cache:clear
-```
-
-For installations from source:
-```
-cd /home/git/gitlab
-sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
-```
-
-## Precompile the assets
-
-Sometimes during version upgrades you might end up with some wrong CSS or
-missing some icons. In that case, try to precompile the assets again.
-
-Note that this only applies to source installations and does NOT apply to
-omnibus packages.
-
-For installations from source:
-```
-cd /home/git/gitlab
-sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
-```
-
-For omnibus versions, the unoptimized assets (JavaScript, CSS) are frozen at
-the release of upstream GitLab. The omnibus version includes optimized versions
-of those assets. Unless you are modifying the JavaScript / CSS code on your
-production machine after installing the package, there should be no reason to redo
-rake assets:precompile on the production machine. If you suspect that assets
-have been corrupted, you should reinstall the omnibus package.
-
-## Tracking Deployments
-
-GitLab provides a Rake task that lets you track deployments in GitLab
-Performance Monitoring. This Rake task simply stores the current GitLab version
-in the GitLab Performance Monitoring database.
-
-For Omnibus-packages:
-
-```
-sudo gitlab-rake gitlab:track_deployment
-```
-
-For installations from source:
-
-```
-cd /home/git/gitlab
-sudo -u git -H bundle exec rake gitlab:track_deployment RAILS_ENV=production
-```
+This document was moved to [administration/raketasks/maintenance](../administration/raketasks/maintenance.md).
diff --git a/doc/update/README.md b/doc/update/README.md
index 975d72164b4..837b31abb97 100644
--- a/doc/update/README.md
+++ b/doc/update/README.md
@@ -85,6 +85,8 @@ possible.
- [MySQL installation guide](../install/database_mysql.md) contains additional
information about configuring GitLab to work with a MySQL database.
- [Restoring from backup after a failed upgrade](restore_after_failure.md)
+- [Upgrading PostgreSQL Using Slony](upgrading_postgresql_using_slony.md), for
+ upgrading a PostgreSQL database with minimal downtime.
[omnidocker]: http://docs.gitlab.com/omnibus/docker/README.html
[source-ee]: https://gitlab.com/gitlab-org/gitlab-ee/tree/master/doc/update
diff --git a/doc/update/upgrading_postgresql_using_slony.md b/doc/update/upgrading_postgresql_using_slony.md
new file mode 100644
index 00000000000..f009906256e
--- /dev/null
+++ b/doc/update/upgrading_postgresql_using_slony.md
@@ -0,0 +1,482 @@
+# Upgrading PostgreSQL Using Slony
+
+This guide describes the steps one can take to upgrade their PostgreSQL database
+to the latest version without the need for hours of downtime. This guide assumes
+you have two database servers: one database server running an older version of
+PostgreSQL (e.g. 9.2.18) and one server running a newer version (e.g. 9.6.0).
+
+For this process we'll use a PostgreSQL replication tool called
+["Slony"](http://www.slony.info/). Slony allows replication between different
+PostgreSQL versions and as such can be used to upgrade a cluster with a minimal
+amount of downtime.
+
+In various places we'll refer to the user `gitlab-psql`. This user should be the
+user used to run the various PostgreSQL OS processes. If you're using a
+different user (e.g. `postgres`) you should replace `gitlab-psql` with the name
+of said user. This guide also assumes your database is called
+`gitlabhq_production`. If you happen to use a different database name you should
+change this accordingly.
+
+## Database Dumps
+
+Slony only replicates data and not any schema changes. As a result we must
+ensure that all databases have the same database structure.
+
+To do so we'll generate a dump of our current database. This dump will only
+contain the structure, not any data. To generate this dump run the following
+command on your active database server:
+
+```bash
+sudo -u gitlab-psql /opt/gitlab/embedded/bin/pg_dump -h /var/opt/gitlab/postgresql -p 5432 -U gitlab-psql -s -f /tmp/structure.sql gitlabhq_production
+```
+
+If you're not using GitLab's Omnibus package you may have to adjust the paths to
+`pg_dump` and the PostgreSQL installation directory to match the paths of your
+configuration.
+
+Once the structure dump is generated we also need to generate a dump for the
+`schema_migrations` table. This table doesn't have any primary keys and as such
+can't be replicated easily by Slony. To generate this dump run the following
+command on your active database server:
+
+```bash
+sudo -u gitlab-psql /opt/gitlab/embedded/bin/pg_dump -h /var/opt/gitlab/postgresql/ -p 5432 -U gitlab-psql -a -t schema_migrations -f /tmp/migrations.sql gitlabhq_production
+```
+
+Next we'll need to move these files somewhere accessible by the new database
+server. The easiest way is to simply download these files to your local system:
+
+```bash
+scp your-user@production-database-host:/tmp/*.sql /tmp
+```
+
+This will copy all the SQL files located in `/tmp` to your local system's
+`/tmp` directory. Once copied you can safely remove the files from the database
+server.
+
+## Installing Slony
+
+Slony will be used to upgrade the database without requiring long downtimes.
+Slony can be downloaded from http://www.slony.info/. If you have installed
+PostgreSQL using your operating system's package manager you may also be able to
+install Slony using said package manager.
+
+When compiling Slony from source you *must* use the following commands to do so:
+
+```bash
+./configure --prefix=/path/to/installation/directory --with-perltools --with-pgconfigdir=/path/to/directory/containing/pg_config/bin
+make
+make install
+```
+
+Omnibus users can use the following commands:
+
+```bash
+./configure --prefix=/opt/gitlab/embedded --with-perltools --with-pgconfigdir=/opt/gitlab/embedded/bin
+make
+make install
+```
+
+This assumes you have installed GitLab into /opt/gitlab.
+
+To test if Slony is installed properly, run the following commands:
+
+```bash
+test -f /opt/gitlab/embedded/bin/slonik && echo 'Slony installed' || echo 'Slony not installed'
+test -f /opt/gitlab/embedded/bin/slonik_init_cluster && echo 'Slony Perl tools are available' || echo 'Slony Perl tools are not available'
+/opt/gitlab/embedded/bin/slonik -v
+```
+
+This assumes Slony was installed to `/opt/gitlab/embedded`. If Slony was
+installed properly the output of these commands will be (the mentioned "slonik"
+version may be different):
+
+```
+Slony installed
+Slony Perl tools are available
+slonik version 2.2.5
+```
+
+## Slony User
+
+Next we must set up a PostgreSQL user that Slony can use to replicate your
+database. To do so, log in to your production database using `psql` using a
+super user account. Once done run the following SQL queries:
+
+```sql
+CREATE ROLE slony WITH SUPERUSER LOGIN REPLICATION ENCRYPTED PASSWORD 'password string here';
+ALTER ROLE slony SET statement_timeout TO 0;
+```
+
+Make sure you replace "password string here" with the actual password for the
+user. A password is *required*. This user must be created on _both_ the old and
+new database server using the same password.
+
+Once the user has been created make sure you note down the password as we will
+need it later on.
+
+## Configuring Slony
+
+Now we can finally start configuring Slony. Slony uses a configuration file for
+most of the work so we'll need to set this one up. This configuration file
+specifies where to put log files, how Slony should connect to the databases,
+etc.
+
+First we'll need to create some required directories and set the correct
+permissions. To do so, run the following commands on both the old and new
+database server:
+
+```bash
+sudo mkdir -p /var/log/gitlab/slony /var/run/slony1 /var/opt/gitlab/postgresql/slony
+sudo chown gitlab-psql:root /var/log/gitlab/slony /var/run/slony1 /var/opt/gitlab/postgresql/slony
+```
+
+Here `gitlab-psql` is the user used to run the PostgreSQL database processes. If
+you're using a different user you should replace this with the name of said
+user.
+
+Now that the directories are in place we can create the configuration file. For
+this we can use the following template:
+
+```perl
+if ($ENV{"SLONYNODES"}) {
+ require $ENV{"SLONYNODES"};
+} else {
+ $CLUSTER_NAME = 'slony_replication';
+ $LOGDIR = '/var/log/gitlab/slony';
+ $MASTERNODE = 1;
+ $DEBUGLEVEL = 2;
+
+ add_node(host => 'OLD_HOST', dbname => 'gitlabhq_production', port =>5432,
+ user=>'slony', password=>'SLONY_PASSWORD', node=>1);
+
+ add_node(host => 'NEW_HOST', dbname => 'gitlabhq_production', port =>5432,
+ user=>'slony', password=>'SLONY_PASSWORD', node=>2, parent=>1 );
+}
+
+$SLONY_SETS = {
+ "set1" => {
+ "set_id" => 1,
+ "table_id" => 1,
+ "sequence_id" => 1,
+ "pkeyedtables" => [
+ TABLES
+ ],
+ },
+};
+
+if ($ENV{"SLONYSET"}) {
+ require $ENV{"SLONYSET"};
+}
+
+# Please do not add or change anything below this point.
+1;
+```
+
+In this configuration file you should replace a few placeholders before you can
+use it. The following placeholders should be replaced:
+
+* `OLD_HOST`: the address of the old database server.
+* `NEW_HOST`: the address of the new database server.
+* `SLONY_PASSWORD`: the password of the Slony user created earlier.
+* `TABLES`: the tables to replicate.
+
+The list of tables to replicate can be generated by running the following
+command on your old PostgreSQL database:
+
+```
+sudo gitlab-psql gitlabhq_production -c "select concat('\"', schemaname, '.', tablename, '\",') from pg_catalog.pg_tables where schemaname = 'public' and tableowner = 'gitlab' and tablename != 'schema_migrations' order by tablename asc;" -t
+```
+
+If you're not using Omnibus you should replace `gitlab-psql` with the
+appropriate path to the `psql` executable.
+
+The above command outputs a list of tables in a format that can be copy-pasted
+directly into the above configuration file. Make sure to _replace_ `TABLES` with
+this output, don't just append it below it. Once done you'll end up with
+something like this:
+
+```perl
+"pkeyedtables" => [
+ "public.abuse_reports",
+ "public.appearances",
+ "public.application_settings",
+ ... more rows here ...
+]
+```
+
+Once you have the configuration file generated you must install it on both the
+old and new database. To do so, place it in
+`/var/opt/gitlab/postgresql/slony/slon_tools.conf` (for which we created the
+directory earlier on).
+
+Now that the configuration file is in place we can _finally_ start replicating
+our database. First we must set up the schema in our new database. To do so make
+sure that the SQL files we generated earlier can be found in the `/tmp`
+directory of the new server. Once these files are in place start a `psql`
+session on this server:
+
+```
+sudo gitlab-psql gitlabhq_production
+```
+
+Now run the following commands:
+
+```
+\i /tmp/structure.sql
+\i /tmp/migrations.sql
+```
+
+To verify if the structure is in place close the session, start it again, then
+run `\d`. If all went well you should see output along the lines of the
+following:
+
+```
+ List of relations
+ Schema | Name | Type | Owner
+--------+---------------------------------------------+----------+-------------
+ public | abuse_reports | table | gitlab
+ public | abuse_reports_id_seq | sequence | gitlab
+ public | appearances | table | gitlab
+ public | appearances_id_seq | sequence | gitlab
+ public | application_settings | table | gitlab
+ public | application_settings_id_seq | sequence | gitlab
+ public | approvals | table | gitlab
+ ... more rows here ...
+```
+
+Now we can initialize the required tables and what not that Slony will use for
+its replication process. To do so, run the following on the old database:
+
+```
+sudo -u gitlab-psql /opt/gitlab/embedded/bin/slonik_init_cluster --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf | /opt/gitlab/embedded/bin/slonik
+```
+
+If all went well this will produce something along the lines of:
+
+```
+<stdin>:10: Set up replication nodes
+<stdin>:13: Next: configure paths for each node/origin
+<stdin>:16: Replication nodes prepared
+<stdin>:17: Please start a slon replication daemon for each node
+```
+
+Next we need to start a replication node on every server. To do so, run the
+following on the old database:
+
+```
+sudo -u gitlab-psql /opt/gitlab/embedded/bin/slon_start 1 --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf
+```
+
+If all went well this will produce output such as:
+
+
+```
+Invoke slon for node 1 - /opt/gitlab/embedded/bin/slon -p /var/run/slony1/slony_replication_node1.pid -s 1000 -d2 slony_replication 'host=192.168.0.7 dbname=gitlabhq_production user=slony port=5432 password=hieng8ezohHuCeiqu0leeghai4aeyahp' > /var/log/gitlab/slony/node1/gitlabhq_production-2016-10-06.log 2>&1 &
+Slon successfully started for cluster slony_replication, node node1
+PID [26740]
+Start the watchdog process as well...
+```
+
+Next we need to run the following command on the _new_ database server:
+
+```
+sudo -u gitlab-psql /opt/gitlab/embedded/bin/slon_start 2 --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf
+```
+
+This will produce similar output if all went well.
+
+Next we need to tell the new database server what it should replicate. This can
+be done by running the following command on the _new_ database server:
+
+```
+sudo -u gitlab-psql /opt/gitlab/embedded/bin/slonik_create_set 1 --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf | /opt/gitlab/embedded/bin/slonik
+```
+
+This should produce output along the lines of the following:
+
+```
+<stdin>:11: Subscription set 1 (set1) created
+<stdin>:12: Adding tables to the subscription set
+<stdin>:16: Add primary keyed table public.abuse_reports
+<stdin>:20: Add primary keyed table public.appearances
+<stdin>:24: Add primary keyed table public.application_settings
+... more rows here ...
+<stdin>:327: Adding sequences to the subscription set
+<stdin>:328: All tables added
+```
+
+Finally we can start the replication process by running the following on the
+_new_ database server:
+
+```
+sudo -u gitlab-psql /opt/gitlab/embedded/bin/slonik_subscribe_set 1 2 --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf | /opt/gitlab/embedded/bin/slonik
+```
+
+This should produce the following output:
+
+```
+<stdin>:6: Subscribed nodes to set 1
+```
+
+At this point the new database server will start replicating the data of the old
+database server. This process can take anywhere from a few minutes to hours, if
+not days. Unfortunately Slony itself doesn't really provide a way of knowing
+when the two databases are in sync. To get an estimate of the progress you can
+use the following shell script:
+
+```
+#!/usr/bin/env bash
+
+set -e
+
+user='slony'
+pass='SLONY_PASSWORD'
+
+function main {
+ while :
+ do
+ local source
+ local target
+
+ source=$(PGUSER="${user}" PGPASSWORD="${pass}" /opt/gitlab/embedded/bin/psql -h OLD_HOST gitlabhq_production -c "select pg_size_pretty(pg_database_size('gitlabhq_production'));" -t -A)
+ target=$(PGUSER="${user}" PGPASSWORD="${pass}" /opt/gitlab/embedded/bin/psql -h NEW_HOST gitlabhq_production -c "select pg_size_pretty(pg_database_size('gitlabhq_production'));" -t -A)
+
+ echo "$(date): ${target} of ${source}" >> progress.log
+ echo "$(date): ${target} of ${source}"
+
+ sleep 60
+ done
+}
+
+main
+```
+
+This script will compare the sizes of the old and new database every minute and
+print the result to STDOUT as well as logging it to a file. Make sure to replace
+`SLONY_PASSWORD`, `OLD_HOST`, and `NEW_HOST` with the correct values.
+
+## Stopping Replication
+
+At some point the two databases are in sync. Once this is the case you'll need
+to plan for a few minutes of downtime. This small downtime window is used to
+stop the replication process, remove any Slony data from both databases, restart
+GitLab so it can use the new database, etc.
+
+First, let's stop all of GitLab. Omnibus users can do so by running the
+following on their GitLab server(s):
+
+```
+sudo gitlab-ctl stop unicorn
+sudo gitlab-ctl stop sidekiq
+sudo gitlab-ctl stop mailroom
+```
+
+If you have any other processes that use PostgreSQL you should also stop those.
+
+Once everything has been stopped you should update any configuration settings,
+DNS records, etc so they all point to the new database.
+
+Once the settings have been taken care of we need to stop the replication
+process. It's crucial that no new data is written to the databases at this point
+as this data will be lost.
+
+To stop replication, run the following on both database servers:
+
+```bash
+sudo -u gitlab-psql /opt/gitlab/embedded/bin/slon_kill --conf /var/opt/gitlab/postgresql/slony/slon_tools.conf
+```
+
+This will stop all the Slony processes on the host the command was executed on.
+
+## Resetting Sequences
+
+The above setup does not replicate database sequences, as such these must be
+reset manually in the target database. You can use the following script for
+this:
+
+```bash
+#!/usr/bin/env bash
+set -e
+
+function main {
+ local fix_sequences
+ local fix_owners
+
+ fix_sequences='/tmp/fix_sequences.sql'
+ fix_owners='/tmp/fix_owners.sql'
+
+ # The SQL queries were taken from
+ # https://wiki.postgresql.org/wiki/Fixing_Sequences
+ sudo gitlab-psql gitlabhq_production -t -c "
+ SELECT 'ALTER SEQUENCE '|| quote_ident(MIN(schema_name)) ||'.'|| quote_ident(MIN(seq_name))
+ ||' OWNED BY '|| quote_ident(MIN(TABLE_NAME)) ||'.'|| quote_ident(MIN(column_name)) ||';'
+ FROM (
+ SELECT
+ n.nspname AS schema_name,
+ c.relname AS TABLE_NAME,
+ a.attname AS column_name,
+ SUBSTRING(d.adsrc FROM E'^nextval\\(''([^'']*)''(?:::text|::regclass)?\\)') AS seq_name
+ FROM pg_class c
+ JOIN pg_attribute a ON (c.oid=a.attrelid)
+ JOIN pg_attrdef d ON (a.attrelid=d.adrelid AND a.attnum=d.adnum)
+ JOIN pg_namespace n ON (c.relnamespace=n.oid)
+ WHERE has_schema_privilege(n.oid,'USAGE')
+ AND n.nspname NOT LIKE 'pg!_%' escape '!'
+ AND has_table_privilege(c.oid,'SELECT')
+ AND (NOT a.attisdropped)
+ AND d.adsrc ~ '^nextval'
+ ) seq
+ GROUP BY seq_name HAVING COUNT(*)=1;
+ " > "${fix_owners}"
+
+ sudo gitlab-psql gitlabhq_production -t -c "
+ SELECT 'SELECT SETVAL(' ||
+ quote_literal(quote_ident(PGT.schemaname) || '.' || quote_ident(S.relname)) ||
+ ', COALESCE(MAX(' ||quote_ident(C.attname)|| '), 1) ) FROM ' ||
+ quote_ident(PGT.schemaname)|| '.'||quote_ident(T.relname)|| ';'
+ FROM pg_class AS S,
+ pg_depend AS D,
+ pg_class AS T,
+ pg_attribute AS C,
+ pg_tables AS PGT
+ WHERE S.relkind = 'S'
+ AND S.oid = D.objid
+ AND D.refobjid = T.oid
+ AND D.refobjid = C.attrelid
+ AND D.refobjsubid = C.attnum
+ AND T.relname = PGT.tablename
+ ORDER BY S.relname;
+ " > "${fix_sequences}"
+
+ sudo gitlab-psql gitlabhq_production -f "${fix_owners}"
+ sudo gitlab-psql gitlabhq_production -f "${fix_sequences}"
+
+ rm "${fix_owners}" "${fix_sequences}"
+}
+
+main
+```
+
+Upload this script to the _target_ server and execute it as follows:
+
+```bash
+bash path/to/the/script/above.sh
+```
+
+This will correct the ownership of sequences and reset the next value for the
+`id` column to the next available value.
+
+## Removing Slony
+
+Next we need to remove all Slony related data. To do so, run the following
+command on the _target_ server:
+
+```bash
+sudo gitlab-psql gitlabhq_production -c "DROP SCHEMA _slony_replication CASCADE;"
+```
+
+Once done you can safely remove any Slony related files (e.g. the log
+directory), and uninstall Slony if desired. At this point you can start your
+GitLab instance again and if all went well it should be using your new database
+server.
diff --git a/features/dashboard/active_tab.feature b/features/dashboard/active_tab.feature
index 08b87808f33..bd883a0ebfa 100644
--- a/features/dashboard/active_tab.feature
+++ b/features/dashboard/active_tab.feature
@@ -18,7 +18,7 @@ Feature: Dashboard Active Tab
Then the active main tab should be Merge Requests
And no other main tabs should be active
- Scenario: On Dashboard Help
- Given I visit dashboard help page
- Then the active main tab should be Help
+ Scenario: On Dashboard Groups
+ Given I visit dashboard groups page
+ Then the active main tab should be Groups
And no other main tabs should be active
diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature
index b1d5e4a7acb..92061dac7f4 100644
--- a/features/dashboard/dashboard.feature
+++ b/features/dashboard/dashboard.feature
@@ -11,7 +11,6 @@ Feature: Dashboard
And I visit dashboard page
Scenario: I should see projects list
- Then I should see "New Project" link
Then I should see "Shop" project link
Then I should see "Shop" project CI status
diff --git a/features/steps/dashboard/help.rb b/features/steps/dashboard/help.rb
index 9c94dc70df0..3c5bf44c538 100644
--- a/features/steps/dashboard/help.rb
+++ b/features/steps/dashboard/help.rb
@@ -8,7 +8,7 @@ class Spinach::Features::DashboardHelp < Spinach::FeatureSteps
end
step 'I visit the "Rake Tasks" help page' do
- visit help_page_path("raketasks/maintenance")
+ visit help_page_path("administration/raketasks/maintenance")
end
step 'I should see "Rake Tasks" page markdown rendered' do
diff --git a/features/steps/shared/sidebar_active_tab.rb b/features/steps/shared/sidebar_active_tab.rb
index 5c47238777f..07fff16e867 100644
--- a/features/steps/shared/sidebar_active_tab.rb
+++ b/features/steps/shared/sidebar_active_tab.rb
@@ -1,12 +1,8 @@
module SharedSidebarActiveTab
include Spinach::DSL
- step 'the active main tab should be Help' do
- ensure_active_main_tab('Help')
- end
-
step 'no other main tabs should be active' do
- expect(page).to have_selector('.nav-sidebar > li.active', count: 1)
+ expect(page).to have_selector('.nav-sidebar li.active', count: 1)
end
def ensure_active_main_tab(content)
@@ -17,6 +13,10 @@ module SharedSidebarActiveTab
ensure_active_main_tab('Projects')
end
+ step 'the active main tab should be Groups' do
+ ensure_active_main_tab('Groups')
+ end
+
step 'the active main tab should be Projects' do
ensure_active_main_tab('Projects')
end
@@ -28,8 +28,4 @@ module SharedSidebarActiveTab
step 'the active main tab should be Merge Requests' do
ensure_active_main_tab('Merge Requests')
end
-
- step 'the active main tab should be Help' do
- ensure_active_main_tab('Help')
- end
end
diff --git a/generator_templates/rails/post_deployment_migration/migration.rb b/generator_templates/rails/post_deployment_migration/migration.rb
new file mode 100644
index 00000000000..1a7b8d5bf35
--- /dev/null
+++ b/generator_templates/rails/post_deployment_migration/migration.rb
@@ -0,0 +1,22 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class <%= migration_class_name %> < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ end
+end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 8025581d3ca..3c9d7b1aaef 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -1,18 +1,12 @@
module API
module Helpers
+ include Gitlab::Utils
+
PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
PRIVATE_TOKEN_PARAM = :private_token
SUDO_HEADER = "HTTP_SUDO"
SUDO_PARAM = :sudo
- def to_boolean(value)
- return value if [true, false].include?(value)
- return true if value =~ /^(true|t|yes|y|1|on)$/i
- return false if value =~ /^(false|f|no|n|0|off)$/i
-
- nil
- end
-
def private_token
params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
end
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index 14f5be3b5f6..dd93a85dc54 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -47,7 +47,8 @@ module API
:build_events,
:pipeline_events,
:wiki_page_events,
- :enable_ssl_verification
+ :enable_ssl_verification,
+ :token
]
@hook = user_project.hooks.new(attrs)
@@ -82,7 +83,8 @@ module API
:build_events,
:pipeline_events,
:wiki_page_events,
- :enable_ssl_verification
+ :enable_ssl_verification,
+ :token
]
if @hook.update_attributes attrs
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index cb213a76a05..3740d4fb4cd 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -102,10 +102,10 @@ module Banzai
end
elsif element_node?(node)
- yield_valid_link(node) do |link, text|
+ yield_valid_link(node) do |link, inner_html|
if ref_pattern && link =~ /\A#{ref_pattern}\z/
replace_link_node_with_href(node, link) do
- object_link_filter(link, ref_pattern, link_text: text)
+ object_link_filter(link, ref_pattern, link_content: inner_html)
end
next
@@ -113,9 +113,9 @@ module Banzai
next unless link_pattern
- if link == text && text =~ /\A#{link_pattern}/
+ if link == inner_html && inner_html =~ /\A#{link_pattern}/
replace_link_node_with_text(node, link) do
- object_link_filter(text, link_pattern)
+ object_link_filter(inner_html, link_pattern)
end
next
@@ -123,7 +123,7 @@ module Banzai
if link =~ /\A#{link_pattern}\z/
replace_link_node_with_href(node, link) do
- object_link_filter(link, link_pattern, link_text: text)
+ object_link_filter(link, link_pattern, link_content: inner_html)
end
next
@@ -140,11 +140,11 @@ module Banzai
#
# text - String text to replace references in.
# pattern - Reference pattern to match against.
- # link_text - Original content of the link being replaced.
+ # link_content - Original content of the link being replaced.
#
# Returns a String with references replaced with links. All links
# have `gfm` and `gfm-OBJECT_NAME` class names attached for styling.
- def object_link_filter(text, pattern, link_text: nil)
+ def object_link_filter(text, pattern, link_content: nil)
references_in(text, pattern) do |match, id, project_ref, matches|
project = project_from_ref_cached(project_ref)
@@ -152,7 +152,7 @@ module Banzai
title = object_link_title(object)
klass = reference_class(object_sym)
- data = data_attributes_for(link_text || match, project, object)
+ data = data_attributes_for(link_content || match, project, object)
if matches.names.include?("url") && matches[:url]
url = matches[:url]
@@ -160,11 +160,11 @@ module Banzai
url = url_for_object_cached(object, project)
end
- text = link_text || object_link_text(object, matches)
+ content = link_content || object_link_text(object, matches)
%(<a href="#{url}" #{data}
title="#{escape_once(title)}"
- class="#{klass}">#{escape_once(text)}</a>)
+ class="#{klass}">#{content}</a>)
else
match
end
diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb
index 0d20be557a0..dce4de3ceaf 100644
--- a/lib/banzai/filter/external_issue_reference_filter.rb
+++ b/lib/banzai/filter/external_issue_reference_filter.rb
@@ -37,10 +37,10 @@ module Banzai
end
elsif element_node?(node)
- yield_valid_link(node) do |link, text|
+ yield_valid_link(node) do |link, inner_html|
if link =~ ref_start_pattern
replace_link_node_with_href(node, link) do
- issue_link_filter(link, link_text: text)
+ issue_link_filter(link, link_content: inner_html)
end
end
end
@@ -54,10 +54,11 @@ module Banzai
# issue's details page.
#
# text - String text to replace references in.
+ # link_content - Original content of the link being replaced.
#
# Returns a String with `JIRA-123` references replaced with links. All
# links have `gfm` and `gfm-issue` class names attached for styling.
- def issue_link_filter(text, link_text: nil)
+ def issue_link_filter(text, link_content: nil)
project = context[:project]
self.class.references_in(text, issue_reference_pattern) do |match, id|
@@ -69,11 +70,11 @@ module Banzai
klass = reference_class(:issue)
data = data_attribute(project: project.id, external_issue: id)
- text = link_text || match
+ content = link_content || match
%(<a href="#{url}" #{data}
title="#{escape_once(title)}"
- class="#{klass}">#{escape_once(text)}</a>)
+ class="#{klass}">#{content}</a>)
end
end
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index 2d221290f7e..84bfeac8041 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -85,14 +85,14 @@ module Banzai
@nodes ||= each_node.to_a
end
- # Yields the link's URL and text whenever the node is a valid <a> tag.
+ # Yields the link's URL and inner HTML whenever the node is a valid <a> tag.
def yield_valid_link(node)
link = CGI.unescape(node.attr('href').to_s)
- text = node.text
+ inner_html = node.inner_html
return unless link.force_encoding('UTF-8').valid_encoding?
- yield link, text
+ yield link, inner_html
end
def replace_text_when_pattern_matches(node, pattern)
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index c6302b586d3..f842b1fb779 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -35,10 +35,10 @@ module Banzai
user_link_filter(content)
end
elsif element_node?(node)
- yield_valid_link(node) do |link, text|
+ yield_valid_link(node) do |link, inner_html|
if link =~ ref_pattern_start
replace_link_node_with_href(node, link) do
- user_link_filter(link, link_text: text)
+ user_link_filter(link, link_content: inner_html)
end
end
end
@@ -52,15 +52,16 @@ module Banzai
# user's profile page.
#
# text - String text to replace references in.
+ # link_content - Original content of the link being replaced.
#
# Returns a String with `@user` references replaced with links. All links
# have `gfm` and `gfm-project_member` class names attached for styling.
- def user_link_filter(text, link_text: nil)
+ def user_link_filter(text, link_content: nil)
self.class.references_in(text) do |match, username|
if username == 'all'
- link_to_all(link_text: link_text)
+ link_to_all(link_content: link_content)
elsif namespace = namespaces[username]
- link_to_namespace(namespace, link_text: link_text) || match
+ link_to_namespace(namespace, link_content: link_content) || match
else
match
end
@@ -102,49 +103,49 @@ module Banzai
reference_class(:project_member)
end
- def link_to_all(link_text: nil)
+ def link_to_all(link_content: nil)
project = context[:project]
author = context[:author]
if author && !project.team.member?(author)
- link_text
+ link_content
else
url = urls.namespace_project_url(project.namespace, project,
only_path: context[:only_path])
data = data_attribute(project: project.id, author: author.try(:id))
- text = link_text || User.reference_prefix + 'all'
+ content = link_content || User.reference_prefix + 'all'
- link_tag(url, data, text, 'All Project and Group Members')
+ link_tag(url, data, content, 'All Project and Group Members')
end
end
- def link_to_namespace(namespace, link_text: nil)
+ def link_to_namespace(namespace, link_content: nil)
if namespace.is_a?(Group)
- link_to_group(namespace.path, namespace, link_text: link_text)
+ link_to_group(namespace.path, namespace, link_content: link_content)
else
- link_to_user(namespace.path, namespace, link_text: link_text)
+ link_to_user(namespace.path, namespace, link_content: link_content)
end
end
- def link_to_group(group, namespace, link_text: nil)
+ def link_to_group(group, namespace, link_content: nil)
url = urls.group_url(group, only_path: context[:only_path])
data = data_attribute(group: namespace.id)
- text = link_text || Group.reference_prefix + group
+ content = link_content || Group.reference_prefix + group
- link_tag(url, data, text, namespace.name)
+ link_tag(url, data, content, namespace.name)
end
- def link_to_user(user, namespace, link_text: nil)
+ def link_to_user(user, namespace, link_content: nil)
url = urls.user_url(user, only_path: context[:only_path])
data = data_attribute(user: namespace.owner_id)
- text = link_text || User.reference_prefix + user
+ content = link_content || User.reference_prefix + user
- link_tag(url, data, text, namespace.owner_name)
+ link_tag(url, data, content, namespace.owner_name)
end
- def link_tag(url, data, text, title)
- %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{escape_once(text)}</a>)
+ def link_tag(url, data, link_content, title)
+ %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>)
end
end
end
diff --git a/lib/banzai/redactor.rb b/lib/banzai/redactor.rb
index 0df3a72d1c4..de3ebe72720 100644
--- a/lib/banzai/redactor.rb
+++ b/lib/banzai/redactor.rb
@@ -41,10 +41,10 @@ module Banzai
next if visible.include?(node)
doc_data[:visible_reference_count] -= 1
- # The reference should be replaced by the original text,
- # which is not always the same as the rendered text.
- text = node.attr('data-original') || node.text
- node.replace(text)
+ # The reference should be replaced by the original link's content,
+ # which is not always the same as the rendered one.
+ content = node.attr('data-original') || node.inner_html
+ node.replace(content)
end
end
diff --git a/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb b/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb
new file mode 100644
index 00000000000..7cb4bccb23c
--- /dev/null
+++ b/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb
@@ -0,0 +1,15 @@
+require 'rails/generators'
+
+module Rails
+ class PostDeploymentMigrationGenerator < Rails::Generators::NamedBase
+ def create_migration_file
+ timestamp = Time.now.strftime('%Y%m%d%H%I%S')
+
+ template "migration.rb", "db/post_migrate/#{timestamp}_#{file_name}.rb"
+ end
+
+ def migration_class_name
+ file_name.camelize
+ end
+ end
+end
diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb
index e59ead5d76c..4c395b4266e 100644
--- a/lib/gitlab/utils.rb
+++ b/lib/gitlab/utils.rb
@@ -13,5 +13,13 @@ module Gitlab
def force_utf8(str)
str.force_encoding(Encoding::UTF_8)
end
+
+ def to_boolean(value)
+ return value if [true, false].include?(value)
+ return true if value =~ /^(true|t|yes|y|1|on)$/i
+ return false if value =~ /^(false|f|no|n|0|off)$/i
+
+ nil
+ end
end
end
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index 210899882b4..58761a129d4 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -63,11 +63,11 @@ namespace :gitlab do
# Launch installation process
system(*%W(bin/install) + repository_storage_paths_args)
-
- # (Re)create hooks
- system(*%W(bin/create-hooks) + repository_storage_paths_args)
end
+ # (Re)create hooks
+ Rake::Task['gitlab:shell:create_hooks'].invoke
+
# Required for debian packaging with PKGR: Setup .ssh/environment with
# the current PATH, so that the correct ruby version gets loaded
# Requires to set "PermitUserEnvironment yes" in sshd config (should not
@@ -102,6 +102,15 @@ namespace :gitlab do
end
end
end
+
+ desc 'Create or repair repository hooks symlink'
+ task create_hooks: :environment do
+ warn_user_is_not_gitlab
+
+ puts 'Creating/Repairing hooks symlinks for all repositories'
+ system(*%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args)
+ puts 'done'.color(:green)
+ end
end
def setup
diff --git a/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb
new file mode 100644
index 00000000000..da167dc570f
--- /dev/null
+++ b/spec/bin/changelog_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+load File.expand_path('../../bin/changelog', __dir__)
+
+describe 'bin/changelog' do
+ describe ChangelogOptionParser do
+ it 'parses --ammend' do
+ options = described_class.parse(%w[foo bar --amend])
+
+ expect(options.amend).to eq true
+ end
+
+ it 'parses --merge-request' do
+ options = described_class.parse(%w[foo --merge-request 1234 bar])
+
+ expect(options.merge_request).to eq 1234
+ end
+
+ it 'parses -m' do
+ options = described_class.parse(%w[foo -m 4321 bar])
+
+ expect(options.merge_request).to eq 4321
+ end
+
+ it 'parses --dry-run' do
+ options = described_class.parse(%w[foo --dry-run bar])
+
+ expect(options.dry_run).to eq true
+ end
+
+ it 'parses -n' do
+ options = described_class.parse(%w[foo -n bar])
+
+ expect(options.dry_run).to eq true
+ end
+
+ it 'parses --git-username' do
+ allow(described_class).to receive(:git_user_name).and_return('Jane Doe')
+ options = described_class.parse(%w[foo --git-username bar])
+
+ expect(options.author).to eq 'Jane Doe'
+ end
+
+ it 'parses -u' do
+ allow(described_class).to receive(:git_user_name).and_return('John Smith')
+ options = described_class.parse(%w[foo -u bar])
+
+ expect(options.author).to eq 'John Smith'
+ end
+
+ it 'parses -h' do
+ expect do
+ $stdout = StringIO.new
+
+ described_class.parse(%w[foo -h bar])
+ end.to raise_error(SystemExit)
+ end
+
+ it 'assigns title' do
+ options = described_class.parse(%W[foo -m 1 bar\n -u baz\r\n --amend])
+
+ expect(options.title).to eq 'foo bar baz'
+ end
+ end
+end
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index b4f066d8600..2a7523c6512 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -14,49 +14,49 @@ describe Projects::ProjectMembersController do
end
describe 'POST create' do
- context 'when users are added' do
- let(:project_user) { create(:user) }
+ let(:project_user) { create(:user) }
- before { sign_in(user) }
+ before { sign_in(user) }
- context 'when user does not have enough rights' do
- before { project.team << [user, :developer] }
+ context 'when user does not have enough rights' do
+ before { project.team << [user, :developer] }
- it 'returns 404' do
- post :create, namespace_id: project.namespace,
- project_id: project,
- user_ids: project_user.id,
- access_level: Gitlab::Access::GUEST
+ it 'returns 404' do
+ post :create, namespace_id: project.namespace,
+ project_id: project,
+ user_ids: project_user.id,
+ access_level: Gitlab::Access::GUEST
- expect(response).to have_http_status(404)
- expect(project.users).not_to include project_user
- end
+ expect(response).to have_http_status(404)
+ expect(project.users).not_to include project_user
end
+ end
- context 'when user has enough rights' do
- before { project.team << [user, :master] }
+ context 'when user has enough rights' do
+ before { project.team << [user, :master] }
- it 'adds user to members' do
- post :create, namespace_id: project.namespace,
- project_id: project,
- user_ids: project_user.id,
- access_level: Gitlab::Access::GUEST
-
- expect(response).to set_flash.to 'Users were successfully added.'
- expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project))
- expect(project.users).to include project_user
- end
+ it 'adds user to members' do
+ expect_any_instance_of(Members::CreateService).to receive(:execute).and_return(true)
- it 'adds no user to members' do
- post :create, namespace_id: project.namespace,
- project_id: project,
- user_ids: '',
- access_level: Gitlab::Access::GUEST
+ post :create, namespace_id: project.namespace,
+ project_id: project,
+ user_ids: project_user.id,
+ access_level: Gitlab::Access::GUEST
- expect(response).to set_flash.to 'No users or groups specified.'
- expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project))
- expect(project.users).not_to include project_user
- end
+ expect(response).to set_flash.to 'Users were successfully added.'
+ expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project))
+ end
+
+ it 'adds no user to members' do
+ expect_any_instance_of(Members::CreateService).to receive(:execute).and_return(false)
+
+ post :create, namespace_id: project.namespace,
+ project_id: project,
+ user_ids: '',
+ access_level: Gitlab::Access::GUEST
+
+ expect(response).to set_flash.to 'No users or groups specified.'
+ expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project))
end
end
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 3234fabe288..a92075fec8f 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -53,7 +53,7 @@ describe 'Issue Boards', feature: true, js: true do
context 'with lists' do
let(:milestone) { create(:milestone, project: project) }
- let(:planning) { create(:label, project: project, name: 'Planning') }
+ let(:planning) { create(:label, project: project, name: 'Planning', description: 'Test') }
let(:development) { create(:label, project: project, name: 'Development') }
let(:testing) { create(:label, project: project, name: 'Testing') }
let(:bug) { create(:label, project: project, name: 'Bug') }
@@ -91,6 +91,12 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_selector('.board', count: 4)
end
+ it 'shows description tooltip on list title' do
+ page.within('.board:nth-child(2)') do
+ expect(find('.board-title span.has-tooltip')[:title]).to eq('Test')
+ end
+ end
+
it 'shows issues in lists' do
wait_for_board_cards(2, 2)
wait_for_board_cards(3, 2)
diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb
index 88e1549a22b..9dfa5d1de19 100644
--- a/spec/features/issues/filter_by_milestone_spec.rb
+++ b/spec/features/issues/filter_by_milestone_spec.rb
@@ -11,6 +11,7 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project)
filter_by_milestone(Milestone::None.title)
+ expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'No Milestone')
expect(page).to have_css('.issue', count: 1)
end
@@ -22,6 +23,7 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project)
filter_by_milestone(Milestone::Upcoming.title)
+ expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming')
expect(page).to have_css('.issue', count: 0)
end
@@ -33,6 +35,7 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project)
filter_by_milestone(Milestone::Upcoming.title)
+ expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming')
expect(page).to have_css('.issue', count: 1)
end
@@ -44,6 +47,7 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project)
filter_by_milestone(Milestone::Upcoming.title)
+ expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming')
expect(page).to have_css('.issue', count: 0)
end
end
@@ -55,6 +59,7 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project)
filter_by_milestone(milestone.title)
+ expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title)
expect(page).to have_css('.issue', count: 1)
end
@@ -70,6 +75,7 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project)
filter_by_milestone(milestone.title)
+ expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title)
expect(page).to have_css('.issue', count: 1)
end
end
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index b963d1305b5..c68e1ea4af9 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -59,4 +59,12 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).to have_css('a.btn.active', text: 'Side-by-side')
end
end
+
+ it 'does not allow non-existing branches' do
+ visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_branch: 'non-exist-target', source_branch: 'non-exist-source' })
+
+ expect(page).to have_content('The form contains the following errors')
+ expect(page).to have_content('Source branch "non-exist-source" does not exist')
+ expect(page).to have_content('Target branch "non-exist-target" does not exist')
+ end
end
diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb
index d1685f95503..63a23a14f20 100644
--- a/spec/features/projects/builds_spec.rb
+++ b/spec/features/projects/builds_spec.rb
@@ -216,7 +216,9 @@ describe "Builds" do
@build.run!
visit namespace_project_build_path(@project.namespace, @project, @build)
click_link 'Cancel'
- click_link 'Retry'
+ page.within('.build-header') do
+ click_link 'Retry build'
+ end
end
it 'shows the right status and buttons' do
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index d25cf7fb353..e796ee570b7 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -164,5 +164,23 @@ describe 'Edit Project Settings', feature: true do
expect(page).to have_content "Customize your workflow!"
end
+
+ it "hides project activity tabs" do
+ select "Disabled", from: "project_project_feature_attributes_repository_access_level"
+ select "Disabled", from: "project_project_feature_attributes_issues_access_level"
+ select "Disabled", from: "project_project_feature_attributes_wiki_access_level"
+
+ click_button "Save changes"
+ wait_for_ajax
+
+ visit activity_namespace_project_path(project.namespace, project)
+
+ page.within(".event-filter") do
+ expect(page).to have_selector("a", count: 2)
+ expect(page).not_to have_content("Push events")
+ expect(page).not_to have_content("Merge events")
+ expect(page).not_to have_content("Comments")
+ end
+ end
end
end
diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
new file mode 100644
index 00000000000..b4f5f6b3fc5
--- /dev/null
+++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe 'Projects > Wiki > User views wiki in project page', feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ context 'when repository is disabled for project' do
+ before do
+ project.project_feature.update!(
+ repository_access_level: ProjectFeature::DISABLED,
+ merge_requests_access_level: ProjectFeature::DISABLED,
+ builds_access_level: ProjectFeature::DISABLED
+ )
+ end
+
+ context 'when wiki homepage contains a link' do
+ before do
+ WikiPages::CreateService.new(
+ project,
+ user,
+ title: 'home',
+ content: '[some link](other-page)'
+ ).execute
+ end
+
+ it 'displays the correct URL for the link' do
+ visit namespace_project_path(project.namespace, project)
+ expect(page).to have_link(
+ 'some link',
+ href: namespace_project_wiki_path(
+ project.namespace,
+ project,
+ 'other-page'
+ )
+ )
+ end
+ end
+ end
+end
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index 230543cd175..3ae83ac082d 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -13,7 +13,7 @@ describe 'Dashboard Todos', feature: true do
visit dashboard_todos_path
end
it 'shows "All done" message' do
- expect(page).to have_content "You're all done!"
+ expect(page).to have_content "Todos let you see what you should do next."
end
end
@@ -44,7 +44,7 @@ describe 'Dashboard Todos', feature: true do
end
it 'shows "All done" message' do
- expect(page).to have_content("You're all done!")
+ expect(page).to have_content("Good job! Looks like you don't have any todos left.")
end
end
@@ -64,7 +64,7 @@ describe 'Dashboard Todos', feature: true do
end
it 'shows "All done" message' do
- expect(page).to have_content("You're all done!")
+ expect(page).to have_content("Good job! Looks like you don't have any todos left.")
end
end
end
@@ -152,7 +152,7 @@ describe 'Dashboard Todos', feature: true do
within('.todos-pending-count') { expect(page).to have_content '0' }
expect(page).to have_content 'To do 0'
expect(page).to have_content 'Done 0'
- expect(page).to have_content "You're all done!"
+ expect(page).to have_content "Good job! Looks like you don't have any todos left."
end
end
end
diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js
index bdce2465fbf..9cb8243ee2c 100644
--- a/spec/javascripts/spec_helper.js
+++ b/spec/javascripts/spec_helper.js
@@ -28,7 +28,7 @@
// setTimeout(Teaspoon.execute, 1000)
// Matching files
// By default Teaspoon will look for files that match
-// _spec.{js,js.coffee,.coffee}. Add a filename_spec.js file in your spec path
+// _spec.{js,js.es6}. Add a filename_spec.js file in your spec path
// and it'll be included in the default suite automatically. If you want to
// customize suites, check out the configuration in teaspoon_env.rb
// Manifest
diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
index 2f9343fadaf..fbf7a461fa5 100644
--- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
@@ -8,6 +8,8 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do
end
shared_examples_for "external issue tracker" do
+ it_behaves_like 'a reference containing an element node'
+
it 'requires project context' do
expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
end
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index a2025672ad9..8f0b2db3e8e 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -22,6 +22,8 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
end
context 'internal reference' do
+ it_behaves_like 'a reference containing an element node'
+
let(:reference) { issue.to_reference }
it 'ignores valid references when using non-default tracker' do
@@ -83,6 +85,20 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
expect(link.attr('data-issue')).to eq issue.id.to_s
end
+ it 'includes a data-original attribute' do
+ doc = reference_filter("See #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-original')
+ expect(link.attr('data-original')).to eq reference
+ end
+
+ it 'does not escape the data-original attribute' do
+ inner_html = 'element <code>node</code> inside'
+ doc = reference_filter(%{<a href="#{reference}">#{inner_html}</a>})
+ expect(doc.children.first.attr('data-original')).to eq inner_html
+ end
+
it 'supports an :only_path context' do
doc = reference_filter("Issue #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
@@ -101,6 +117,8 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
end
context 'cross-project reference' do
+ it_behaves_like 'a reference containing an element node'
+
let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:empty_project, :public, namespace: namespace) }
let(:issue) { create(:issue, project: project2) }
@@ -141,6 +159,8 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
end
context 'cross-project URL reference' do
+ it_behaves_like 'a reference containing an element node'
+
let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:empty_project, :public, namespace: namespace) }
let(:issue) { create(:issue, project: project2) }
@@ -160,39 +180,45 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
end
context 'cross-project reference in link href' do
+ it_behaves_like 'a reference containing an element node'
+
let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:empty_project, :public, namespace: namespace) }
let(:issue) { create(:issue, project: project2) }
- let(:reference) { %Q{<a href="#{issue.to_reference(project)}">Reference</a>} }
+ let(:reference) { issue.to_reference(project) }
+ let(:reference_link) { %{<a href="#{reference}">Reference</a>} }
it 'links to a valid reference' do
- doc = reference_filter("See #{reference}")
+ doc = reference_filter("See #{reference_link}")
expect(doc.css('a').first.attr('href')).
to eq helper.url_for_issue(issue.iid, project2)
end
it 'links with adjacent text' do
- doc = reference_filter("Fixed (#{reference}.)")
+ doc = reference_filter("Fixed (#{reference_link}.)")
expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
end
end
context 'cross-project URL in link href' do
+ it_behaves_like 'a reference containing an element node'
+
let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:empty_project, :public, namespace: namespace) }
let(:issue) { create(:issue, project: project2) }
- let(:reference) { %Q{<a href="#{helper.url_for_issue(issue.iid, project2) + "#note_123"}">Reference</a>} }
+ let(:reference) { "#{helper.url_for_issue(issue.iid, project2) + "#note_123"}" }
+ let(:reference_link) { %{<a href="#{reference}">Reference</a>} }
it 'links to a valid reference' do
- doc = reference_filter("See #{reference}")
+ doc = reference_filter("See #{reference_link}")
expect(doc.css('a').first.attr('href')).
to eq helper.url_for_issue(issue.iid, project2) + "#note_123"
end
it 'links with adjacent text' do
- doc = reference_filter("Fixed (#{reference}.)")
+ doc = reference_filter("Fixed (#{reference_link}.)")
expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
end
end
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index 729e77fd43f..5bfeb82e738 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -24,6 +24,8 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
end
context 'mentioning @all' do
+ it_behaves_like 'a reference containing an element node'
+
let(:reference) { User.reference_prefix + 'all' }
before do
@@ -60,6 +62,8 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
end
context 'mentioning a user' do
+ it_behaves_like 'a reference containing an element node'
+
it 'links to a User' do
doc = reference_filter("Hey #{reference}")
expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
@@ -89,6 +93,8 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
end
context 'mentioning a group' do
+ it_behaves_like 'a reference containing an element node'
+
let(:group) { create(:group) }
let(:reference) { group.to_reference }
diff --git a/spec/lib/banzai/pipeline/full_pipeline_spec.rb b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
new file mode 100644
index 00000000000..2501b638774
--- /dev/null
+++ b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
@@ -0,0 +1,28 @@
+require 'rails_helper'
+
+describe Banzai::Pipeline::FullPipeline do
+ describe 'References' do
+ let(:project) { create(:empty_project, :public) }
+ let(:issue) { create(:issue, project: project) }
+
+ it 'handles markdown inside a reference' do
+ markdown = "[some `code` inside](#{issue.to_reference})"
+ result = described_class.call(markdown, project: project)
+ link_content = result[:output].css('a').inner_html
+ expect(link_content).to eq('some <code>code</code> inside')
+ end
+
+ it 'sanitizes reference HTML' do
+ link_label = '<script>bad things</script>'
+ markdown = "[#{link_label}](#{issue.to_reference})"
+ result = described_class.to_html(markdown, project: project)
+ expect(result).not_to include(link_label)
+ end
+
+ it 'escapes the data-original attribute on a reference' do
+ markdown = %Q{[">bad things](#{issue.to_reference})}
+ result = described_class.to_html(markdown, project: project)
+ expect(result).to include(%{data-original='\"&gt;bad things'})
+ end
+ end
+end
diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb
index 254657a881d..6d2c141e18b 100644
--- a/spec/lib/banzai/redactor_spec.rb
+++ b/spec/lib/banzai/redactor_spec.rb
@@ -6,39 +6,60 @@ describe Banzai::Redactor do
let(:redactor) { described_class.new(project, user) }
describe '#redact' do
- it 'redacts an Array of documents' do
- doc1 = Nokogiri::HTML.
- fragment('<a class="gfm" data-reference-type="issue">foo</a>')
-
- doc2 = Nokogiri::HTML.
- fragment('<a class="gfm" data-reference-type="issue">bar</a>')
-
- expect(redactor).to receive(:nodes_visible_to_user).and_return([])
-
- redacted_data = redactor.redact([doc1, doc2])
-
- expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2])
- expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([0, 0])
- expect(doc1.to_html).to eq('foo')
- expect(doc2.to_html).to eq('bar')
+ context 'when reference not visible to user' do
+ before do
+ expect(redactor).to receive(:nodes_visible_to_user).and_return([])
+ end
+
+ it 'redacts an array of documents' do
+ doc1 = Nokogiri::HTML.
+ fragment('<a class="gfm" data-reference-type="issue">foo</a>')
+
+ doc2 = Nokogiri::HTML.
+ fragment('<a class="gfm" data-reference-type="issue">bar</a>')
+
+ redacted_data = redactor.redact([doc1, doc2])
+
+ expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2])
+ expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([0, 0])
+ expect(doc1.to_html).to eq('foo')
+ expect(doc2.to_html).to eq('bar')
+ end
+
+ it 'replaces redacted reference with inner HTML' do
+ doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue'>foo</a>")
+ redactor.redact([doc])
+ expect(doc.to_html).to eq('foo')
+ end
+
+ context 'when data-original attribute provided' do
+ let(:original_content) { '<code>foo</code>' }
+ it 'replaces redacted reference with original content' do
+ doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-original='#{original_content}'>bar</a>")
+ redactor.redact([doc])
+ expect(doc.to_html).to eq(original_content)
+ end
+ end
end
- it 'does not redact an Array of documents' do
- doc1_html = '<a class="gfm" data-reference-type="issue">foo</a>'
- doc1 = Nokogiri::HTML.fragment(doc1_html)
+ context 'when reference visible to user' do
+ it 'does not redact an array of documents' do
+ doc1_html = '<a class="gfm" data-reference-type="issue">foo</a>'
+ doc1 = Nokogiri::HTML.fragment(doc1_html)
- doc2_html = '<a class="gfm" data-reference-type="issue">bar</a>'
- doc2 = Nokogiri::HTML.fragment(doc2_html)
+ doc2_html = '<a class="gfm" data-reference-type="issue">bar</a>'
+ doc2 = Nokogiri::HTML.fragment(doc2_html)
- nodes = redactor.document_nodes([doc1, doc2]).map { |x| x[:nodes] }
- expect(redactor).to receive(:nodes_visible_to_user).and_return(nodes.flatten)
+ nodes = redactor.document_nodes([doc1, doc2]).map { |x| x[:nodes] }
+ expect(redactor).to receive(:nodes_visible_to_user).and_return(nodes.flatten)
- redacted_data = redactor.redact([doc1, doc2])
+ redacted_data = redactor.redact([doc1, doc2])
- expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2])
- expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([1, 1])
- expect(doc1.to_html).to eq(doc1_html)
- expect(doc2.to_html).to eq(doc2_html)
+ expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2])
+ expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([1, 1])
+ expect(doc1.to_html).to eq(doc1_html)
+ expect(doc2.to_html).to eq(doc2_html)
+ end
end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index a5aa387f4f7..62aa212f1f6 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -122,6 +122,14 @@ describe Gitlab::GitAccess, lib: true do
describe 'build authentication_abilities permissions' do
let(:authentication_abilities) { build_authentication_abilities }
+ describe 'owner' do
+ let(:project) { create(:project, namespace: user.namespace) }
+
+ context 'pull code' do
+ it { expect(subject).to be_allowed }
+ end
+ end
+
describe 'reporter user' do
before { project.team << [user, :reporter] }
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
new file mode 100644
index 00000000000..d5d87310874
--- /dev/null
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -0,0 +1,35 @@
+describe Gitlab::Utils, lib: true do
+ def to_boolean(value)
+ described_class.to_boolean(value)
+ end
+
+ describe '.to_boolean' do
+ it 'accepts booleans' do
+ expect(to_boolean(true)).to be(true)
+ expect(to_boolean(false)).to be(false)
+ end
+
+ it 'converts a valid string to a boolean' do
+ expect(to_boolean(true)).to be(true)
+ expect(to_boolean('true')).to be(true)
+ expect(to_boolean('YeS')).to be(true)
+ expect(to_boolean('t')).to be(true)
+ expect(to_boolean('1')).to be(true)
+ expect(to_boolean('ON')).to be(true)
+
+ expect(to_boolean('FaLse')).to be(false)
+ expect(to_boolean('F')).to be(false)
+ expect(to_boolean('NO')).to be(false)
+ expect(to_boolean('n')).to be(false)
+ expect(to_boolean('0')).to be(false)
+ expect(to_boolean('oFF')).to be(false)
+ end
+
+ it 'converts an invalid string to nil' do
+ expect(to_boolean('fals')).to be_nil
+ expect(to_boolean('yeah')).to be_nil
+ expect(to_boolean('')).to be_nil
+ expect(to_boolean(nil)).to be_nil
+ end
+ end
+end
diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb
index 5363aea4d22..9041690023f 100644
--- a/spec/models/concerns/project_features_compatibility_spec.rb
+++ b/spec/models/concerns/project_features_compatibility_spec.rb
@@ -22,4 +22,18 @@ describe ProjectFeaturesCompatibility do
expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED)
end
end
+
+ it "converts fields from true to ProjectFeature::ENABLED" do
+ features.each do |feature|
+ project.update_attribute("#{feature}_enabled".to_sym, true)
+ expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::ENABLED)
+ end
+ end
+
+ it "converts fields from false to ProjectFeature::DISABLED" do
+ features.each do |feature|
+ project.update_attribute("#{feature}_enabled".to_sym, false)
+ expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED)
+ end
+ end
end
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index a9f637147d1..a3e9adae4e2 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -1,4 +1,5 @@
require 'spec_helper'
+include Gitlab::Routing.url_helpers
describe JiraService, models: true do
describe "Associations" do
@@ -66,6 +67,27 @@ describe JiraService, models: true do
).once
end
+ it "references the GitLab commit/merge request" do
+ @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
+
+ expect(WebMock).to have_requested(:post, @comment_url).with(
+ body: /#{Gitlab.config.gitlab.url}\/#{project.path_with_namespace}\/commit\/#{merge_request.diff_head_sha}/
+ ).once
+ end
+
+ it "references the GitLab commit/merge request (relative URL)" do
+ stub_config_setting(relative_url_root: '/gitlab')
+ stub_config_setting(url: Settings.send(:build_gitlab_url))
+
+ Project.default_url_options[:script_name] = "/gitlab"
+
+ @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
+
+ expect(WebMock).to have_requested(:post, @comment_url).with(
+ body: /#{Gitlab.config.gitlab.url}\/#{project.path_with_namespace}\/commit\/#{merge_request.diff_head_sha}/
+ ).once
+ end
+
it "calls the api with jira_issue_transition_id" do
@jira_service.jira_issue_transition_id = 'this-is-a-custom-id'
@jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index f4dda1ee558..aef277357cf 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -67,11 +67,11 @@ describe Project, models: true do
it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
it { is_expected.to have_many(:forks).through(:forked_project_links) }
- context 'after create' do
- it "creates project feature" do
+ context 'after initialized' do
+ it "has a project_feature" do
project = FactoryGirl.build(:project)
- expect { project.save }.to change{ project.project_feature.present? }.from(false).to(true)
+ expect(project.project_feature.present?).to be_present
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 10c39b90212..d1ed774a914 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -599,6 +599,80 @@ describe User, models: true do
end
end
+ describe '.search_with_secondary_emails' do
+ def search_with_secondary_emails(query)
+ described_class.search_with_secondary_emails(query)
+ end
+
+ let!(:user) { create(:user) }
+ let!(:email) { create(:email) }
+
+ it 'returns users with a matching name' do
+ expect(search_with_secondary_emails(user.name)).to eq([user])
+ end
+
+ it 'returns users with a partially matching name' do
+ expect(search_with_secondary_emails(user.name[0..2])).to eq([user])
+ end
+
+ it 'returns users with a matching name regardless of the casing' do
+ expect(search_with_secondary_emails(user.name.upcase)).to eq([user])
+ end
+
+ it 'returns users with a matching email' do
+ expect(search_with_secondary_emails(user.email)).to eq([user])
+ end
+
+ it 'returns users with a partially matching email' do
+ expect(search_with_secondary_emails(user.email[0..2])).to eq([user])
+ end
+
+ it 'returns users with a matching email regardless of the casing' do
+ expect(search_with_secondary_emails(user.email.upcase)).to eq([user])
+ end
+
+ it 'returns users with a matching username' do
+ expect(search_with_secondary_emails(user.username)).to eq([user])
+ end
+
+ it 'returns users with a partially matching username' do
+ expect(search_with_secondary_emails(user.username[0..2])).to eq([user])
+ end
+
+ it 'returns users with a matching username regardless of the casing' do
+ expect(search_with_secondary_emails(user.username.upcase)).to eq([user])
+ end
+
+ it 'returns users with a matching whole secondary email' do
+ expect(search_with_secondary_emails(email.email)).to eq([email.user])
+ end
+
+ it 'returns users with a matching part of secondary email' do
+ expect(search_with_secondary_emails(email.email[1..4])).to eq([email.user])
+ end
+
+ it 'return users with a matching part of secondary email regardless of case' do
+ expect(search_with_secondary_emails(email.email[1..4].upcase)).to eq([email.user])
+ expect(search_with_secondary_emails(email.email[1..4].downcase)).to eq([email.user])
+ expect(search_with_secondary_emails(email.email[1..4].capitalize)).to eq([email.user])
+ end
+
+ it 'returns multiple users with matching secondary emails' do
+ email1 = create(:email, email: '1_testemail@example.com')
+ email2 = create(:email, email: '2_testemail@example.com')
+ email3 = create(:email, email: 'other@email.com')
+ email3.user.update_attributes!(email: 'another@mail.com')
+
+ expect(
+ search_with_secondary_emails('testemail@example.com').map(&:id)
+ ).to include(email1.user.id, email2.user.id)
+
+ expect(
+ search_with_secondary_emails('testemail@example.com').map(&:id)
+ ).not_to include(email3.user.id)
+ end
+ end
+
describe 'by_username_or_id' do
let(:user1) { create(:user, username: 'foo') }
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 658e3c13a73..96249a7d8c3 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -6,6 +6,7 @@ describe ProjectPolicy, models: true do
let(:dev) { create(:user) }
let(:master) { create(:user) }
let(:owner) { create(:user) }
+ let(:admin) { create(:admin) }
let(:project) { create(:empty_project, :public, namespace: owner.namespace) }
let(:guest_permissions) do
@@ -155,6 +156,19 @@ describe ProjectPolicy, models: true do
it do
is_expected.to include(*guest_permissions)
is_expected.to include(*reporter_permissions)
+ is_expected.to include(*team_member_reporter_permissions)
+ is_expected.to include(*developer_permissions)
+ is_expected.to include(*master_permissions)
+ is_expected.to include(*owner_permissions)
+ end
+ end
+
+ context 'admin' do
+ let(:current_user) { admin }
+
+ it do
+ is_expected.to include(*guest_permissions)
+ is_expected.to include(*reporter_permissions)
is_expected.not_to include(*team_member_reporter_permissions)
is_expected.to include(*developer_permissions)
is_expected.to include(*master_permissions)
diff --git a/spec/rake_helper.rb b/spec/rake_helper.rb
new file mode 100644
index 00000000000..9b5b4bf9fea
--- /dev/null
+++ b/spec/rake_helper.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+require 'rake'
+
+RSpec.configure do |config|
+ config.include RakeHelpers
+
+ # Redirect stdout so specs don't have so much noise
+ config.before(:all) do
+ $stdout = StringIO.new
+
+ Rake.application.rake_require 'tasks/gitlab/task_helpers'
+ Rake::Task.define_task :environment
+ end
+
+ # Reset stdout
+ config.after(:all) do
+ $stdout = STDOUT
+ end
+end
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index f7fe4c10873..01bb9e955e0 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -265,36 +265,6 @@ describe API::Helpers, api: true do
end
end
- describe '.to_boolean' do
- it 'accepts booleans' do
- expect(to_boolean(true)).to be(true)
- expect(to_boolean(false)).to be(false)
- end
-
- it 'converts a valid string to a boolean' do
- expect(to_boolean(true)).to be(true)
- expect(to_boolean('true')).to be(true)
- expect(to_boolean('YeS')).to be(true)
- expect(to_boolean('t')).to be(true)
- expect(to_boolean('1')).to be(true)
- expect(to_boolean('ON')).to be(true)
-
- expect(to_boolean('FaLse')).to be(false)
- expect(to_boolean('F')).to be(false)
- expect(to_boolean('NO')).to be(false)
- expect(to_boolean('n')).to be(false)
- expect(to_boolean('0')).to be(false)
- expect(to_boolean('oFF')).to be(false)
- end
-
- it 'converts an invalid string to nil' do
- expect(to_boolean('fals')).to be_nil
- expect(to_boolean('yeah')).to be_nil
- expect(to_boolean('')).to be_nil
- expect(to_boolean(nil)).to be_nil
- end
- end
-
describe '.handle_api_exception' do
before do
allow_any_instance_of(self.class).to receive(:sentry_enabled?).and_return(true)
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index cfcdcad74cd..5f39329a1b8 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -88,6 +88,7 @@ describe API::API, 'ProjectHooks', api: true do
expect do
post api("/projects/#{project.id}/hooks", user), url: "http://example.com", issues_events: true
end.to change {project.hooks.count}.by(1)
+
expect(response).to have_http_status(201)
expect(json_response['url']).to eq('http://example.com')
expect(json_response['issues_events']).to eq(true)
@@ -99,6 +100,24 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response['pipeline_events']).to eq(false)
expect(json_response['wiki_page_events']).to eq(false)
expect(json_response['enable_ssl_verification']).to eq(true)
+ expect(json_response).not_to include('token')
+ end
+
+ it "adds the token without including it in the response" do
+ token = "secret token"
+
+ expect do
+ post api("/projects/#{project.id}/hooks", user), url: "http://example.com", token: token
+ end.to change {project.hooks.count}.by(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response["url"]).to eq("http://example.com")
+ expect(json_response).not_to include("token")
+
+ hook = project.hooks.find(json_response["id"])
+
+ expect(hook.url).to eq("http://example.com")
+ expect(hook.token).to eq(token)
end
it "returns a 400 error if url not given" do
@@ -129,6 +148,19 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
end
+ it "adds the token without including it in the response" do
+ token = "secret token"
+
+ put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: "http://example.org", token: token
+
+ expect(response).to have_http_status(200)
+ expect(json_response["url"]).to eq("http://example.org")
+ expect(json_response).not_to include("token")
+
+ expect(hook.reload.url).to eq("http://example.org")
+ expect(hook.reload.token).to eq(token)
+ end
+
it "returns 404 error if hook id not found" do
put api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org'
expect(response).to have_http_status(404)
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index dbdf83a0dff..9bfc84c7425 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -284,7 +284,17 @@ describe 'Git LFS API and storage' do
let(:authorization) { authorize_ci_project }
shared_examples 'can download LFS only from own projects' do
- context 'for own project' do
+ context 'for owned project' do
+ let(:project) { create(:empty_project, namespace: user.namespace) }
+
+ let(:update_permissions) do
+ project.lfs_objects << lfs_object
+ end
+
+ it_behaves_like 'responds with a file'
+ end
+
+ context 'for member of project' do
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:update_permissions) do
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index c64df4979b0..bb26513103d 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -245,6 +245,12 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
it_behaves_like 'a pullable'
end
+
+ context 'when you are owner' do
+ let(:project) { create(:empty_project, namespace: current_user.namespace) }
+
+ it_behaves_like 'a pullable'
+ end
end
context 'for private' do
@@ -266,6 +272,12 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
it_behaves_like 'a pullable'
end
+
+ context 'when you are owner' do
+ let(:project) { create(:empty_project, namespace: current_user.namespace) }
+
+ it_behaves_like 'a pullable'
+ end
end
end
end
@@ -276,13 +288,21 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end
context 'disallow for all' do
- let(:project) { create(:empty_project, :public) }
+ context 'when you are member' do
+ let(:project) { create(:empty_project, :public) }
- before do
- project.team << [current_user, :developer]
+ before do
+ project.team << [current_user, :developer]
+ end
+
+ it_behaves_like 'an inaccessible'
end
- it_behaves_like 'an inaccessible'
+ context 'when you are owner' do
+ let(:project) { create(:empty_project, :public, namespace: current_user.namespace) }
+
+ it_behaves_like 'an inaccessible'
+ end
end
end
end
diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb
new file mode 100644
index 00000000000..0670ac2faa2
--- /dev/null
+++ b/spec/services/members/create_service_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Members::CreateService, services: true do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+ let(:project_user) { create(:user) }
+
+ before { project.team << [user, :master] }
+
+ it 'adds user to members' do
+ params = { user_ids: project_user.id.to_s, access_level: Gitlab::Access::GUEST }
+ result = described_class.new(project, user, params).execute
+
+ expect(result).to be_truthy
+ expect(project.users).to include project_user
+ end
+
+ it 'adds no user to members' do
+ params = { user_ids: '', access_level: Gitlab::Access::GUEST }
+ result = described_class.new(project, user, params).execute
+
+ expect(result).to be_falsey
+ expect(project.users).not_to include project_user
+ end
+end
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 3a3f07ddcb9..3f5df049ea2 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -25,6 +25,8 @@ describe MergeRequests::BuildService, services: true do
before do
allow(CompareService).to receive_message_chain(:new, :execute).and_return(compare)
+ allow(project).to receive(:commit).and_return(commit_1)
+ allow(project).to receive(:commit).and_return(commit_2)
end
describe 'execute' do
@@ -193,5 +195,52 @@ describe MergeRequests::BuildService, services: true do
end
end
end
+
+ context 'source branch does not exist' do
+ before do
+ allow(project).to receive(:commit).with(source_branch).and_return(nil)
+ allow(project).to receive(:commit).with(target_branch).and_return(commit_1)
+ end
+
+ it 'forbids the merge request from being created' do
+ expect(merge_request.can_be_created).to eq(false)
+ end
+
+ it 'adds an error message to the merge request' do
+ expect(merge_request.errors).to contain_exactly('Source branch "feature-branch" does not exist')
+ end
+ end
+
+ context 'target branch does not exist' do
+ before do
+ allow(project).to receive(:commit).with(source_branch).and_return(commit_1)
+ allow(project).to receive(:commit).with(target_branch).and_return(nil)
+ end
+
+ it 'forbids the merge request from being created' do
+ expect(merge_request.can_be_created).to eq(false)
+ end
+
+ it 'adds an error message to the merge request' do
+ expect(merge_request.errors).to contain_exactly('Target branch "master" does not exist')
+ end
+ end
+
+ context 'both source and target branches do not exist' do
+ before do
+ allow(project).to receive(:commit).and_return(nil)
+ end
+
+ it 'forbids the merge request from being created' do
+ expect(merge_request.can_be_created).to eq(false)
+ end
+
+ it 'adds both error messages to the merge request' do
+ expect(merge_request.errors).to contain_exactly(
+ 'Source branch "feature-branch" does not exist',
+ 'Target branch "master" does not exist'
+ )
+ end
+ end
end
end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 3ea1273abc3..876bfaf085c 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -69,7 +69,7 @@ describe Projects::CreateService, services: true do
context 'wiki_enabled false does not create wiki repository directory' do
before do
- @opts.merge!( { project_feature_attributes: { wiki_access_level: ProjectFeature::DISABLED } })
+ @opts.merge!(wiki_enabled: false)
@project = create_project(@user, @opts)
@path = ProjectWiki.new(@project, @user).send(:path_to_repo)
end
diff --git a/spec/support/banzai/reference_filter_shared_examples.rb b/spec/support/banzai/reference_filter_shared_examples.rb
new file mode 100644
index 00000000000..eb5da662ab5
--- /dev/null
+++ b/spec/support/banzai/reference_filter_shared_examples.rb
@@ -0,0 +1,13 @@
+# Specs for reference links containing HTML.
+#
+# Requires a reference:
+# let(:reference) { '#42' }
+shared_examples 'a reference containing an element node' do
+ let(:inner_html) { 'element <code>node</code> inside' }
+ let(:reference_with_element) { %{<a href="#{reference}">#{inner_html}</a>} }
+
+ it 'does not escape inner html' do
+ doc = reference_filter(reference_with_element)
+ expect(doc.children.first.inner_html).to eq(inner_html)
+ end
+end
diff --git a/spec/support/rake_helpers.rb b/spec/support/rake_helpers.rb
new file mode 100644
index 00000000000..52d80c69835
--- /dev/null
+++ b/spec/support/rake_helpers.rb
@@ -0,0 +1,10 @@
+module RakeHelpers
+ def run_rake_task(task_name)
+ Rake::Task[task_name].reenable
+ Rake.application.invoke_task task_name
+ end
+
+ def stub_warn_user_is_not_gitlab
+ allow_any_instance_of(Object).to receive(:warn_user_is_not_gitlab)
+ end
+end
diff --git a/spec/tasks/gitlab/shell_rake_spec.rb b/spec/tasks/gitlab/shell_rake_spec.rb
new file mode 100644
index 00000000000..226d34fe2c9
--- /dev/null
+++ b/spec/tasks/gitlab/shell_rake_spec.rb
@@ -0,0 +1,26 @@
+require 'rake_helper'
+
+describe 'gitlab:shell rake tasks' do
+ before do
+ Rake.application.rake_require 'tasks/gitlab/shell'
+
+ stub_warn_user_is_not_gitlab
+ end
+
+ describe 'install task' do
+ it 'invokes create_hooks task' do
+ expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke)
+
+ run_rake_task('gitlab:shell:install')
+ end
+ end
+
+ describe 'create_hooks task' do
+ it 'calls gitlab-shell bin/create_hooks' do
+ expect_any_instance_of(Object).to receive(:system)
+ .with("#{Gitlab.config.gitlab_shell.path}/bin/create-hooks", *repository_storage_paths_args)
+
+ run_rake_task('gitlab:shell:create_hooks')
+ end
+ end
+end