diff --git a/.pkgr.yml b/.pkgr.yml
index 8fc9fddf8f79d5f5dc391732879b307712d201cf..10bcd7bd4bdf09002cef3f29cf6f0e85551f002a 100644
--- a/.pkgr.yml
+++ b/.pkgr.yml
@@ -3,6 +3,8 @@ group: git
services:
- postgres
before_precompile: ./bin/pkgr_before_precompile.sh
+env:
+ - SKIP_STORAGE_VALIDATION=true
targets:
debian-7: &wheezy
build_dependencies:
@@ -25,6 +27,16 @@ targets:
- libicu52
- libpcre3
- git
+ ubuntu-16.04:
+ build_dependencies:
+ - libkrb5-dev
+ - libicu-dev
+ - cmake
+ - pkg-config
+ dependencies:
+ - libicu55
+ - libpcre3
+ - git
centos-6:
build_dependencies:
- krb5-devel
diff --git a/.rubocop.yml b/.rubocop.yml
index ec72912da15cd6372f6ad66317f995519a04b53a..b43bf49e59d04a5ee8d6af6a0b628d366cf8e83b 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -770,33 +770,26 @@ Rails/ScopeArgs:
RSpec/AnyInstance:
Enabled: false
-# Check for expectations where `be(...)` can replace `eql(...)`.
-RSpec/BeEql:
- Enabled: false
-
-# Check that the first argument to the top level describe is a constant.
+# Check that the first argument to the top level describe is the tested class or
+# module.
RSpec/DescribeClass:
Enabled: false
-# Checks that tests use `described_class`.
-RSpec/DescribedClass:
- Enabled: false
-
-# Checks that the second argument to `describe` specifies a method.
+# Use `described_class` for tested class / module.
RSpec/DescribeMethod:
Enabled: false
-# Checks if an example group does not include any tests.
-RSpec/EmptyExampleGroup:
+# Checks that the second argument to top level describe is the tested method
+# name.
+RSpec/DescribedClass:
Enabled: false
- CustomIncludeMethods: []
-# Checks for long examples.
+# Checks for long example.
RSpec/ExampleLength:
Enabled: false
Max: 5
-# Checks that example descriptions do not start with "should".
+# Do not use should when describing your tests.
RSpec/ExampleWording:
Enabled: false
CustomTransform:
@@ -805,10 +798,6 @@ RSpec/ExampleWording:
not: does not
IgnoredWords: []
-# Checks for `expect(...)` calls containing literal values.
-RSpec/ExpectActual:
- Enabled: false
-
# Checks the file and folder naming of the spec file.
RSpec/FilePath:
Enabled: false
@@ -820,65 +809,19 @@ RSpec/FilePath:
RSpec/Focus:
Enabled: true
-# Checks the arguments passed to `before`, `around`, and `after`.
-RSpec/HookArgument:
- Enabled: false
- EnforcedStyle: implicit
-
-# Check that a consistent implict expectation style is used.
-# TODO (rspeicher): Available in rubocop-rspec 1.8.0
-# RSpec/ImplicitExpect:
-# Enabled: true
-# EnforcedStyle: is_expected
-
# Checks for the usage of instance variables.
RSpec/InstanceVariable:
Enabled: false
-# Checks for `subject` definitions that come after `let` definitions.
-RSpec/LeadingSubject:
- Enabled: false
-
-# Checks unreferenced `let!` calls being used for test setup.
-RSpec/LetSetup:
- Enabled: false
-
-# Check that chains of messages are not being stubbed.
-RSpec/MessageChain:
- Enabled: false
-
-# Checks for consistent message expectation style.
-RSpec/MessageExpectation:
- Enabled: false
- EnforcedStyle: allow
-
-# Checks for multiple top level describes.
+# Checks for multiple top-level describes.
RSpec/MultipleDescribes:
Enabled: false
-# Checks if examples contain too many `expect` calls.
-RSpec/MultipleExpectations:
- Enabled: false
- Max: 1
-
-# Checks for explicitly referenced test subjects.
-RSpec/NamedSubject:
- Enabled: false
-
-# Checks for nested example groups.
-RSpec/NestedGroups:
- Enabled: false
- MaxNesting: 2
-
-# Checks for consistent method usage for negating expectations.
+# Enforces the usage of the same method on all negative message expectations.
RSpec/NotToNot:
EnforcedStyle: not_to
Enabled: true
-# Checks for stubbed test subjects.
-RSpec/SubjectStub:
- Enabled: false
-
# Prefer using verifying doubles over normal doubles.
RSpec/VerifiedDoubles:
Enabled: false
diff --git a/CHANGELOG b/CHANGELOG
index 6206ca977351164efc07e98e310224fec6fa0988..0c6faa56e4d9e1cbabd9ac65261c87ac095a1b17 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,8 @@
Please view this file on the master branch, on stable branches it's out of date.
-v 8.12.0 (unreleased)
+v 8.12.1 (unreleased)
+
+v 8.12.0
- Update the rouge gem to 2.0.6, which adds highlighting support for JSX, Prometheus, and others. !6251
- Only check :can_resolve permission if the note is resolvable
- Bump fog-aws to v0.11.0 to support ap-south-1 region
@@ -11,23 +13,28 @@ v 8.12.0 (unreleased)
- Prune events older than 12 months. (ritave)
- Prepend blank line to `Closes` message on merge request linked to issue (lukehowell)
- Fix issues/merge-request templates dropdown for forked projects
+ - Amends the packager.io configuration file to create a build for Ubuntu 16.04. !6247 (Jon "The Nice Guy" Spriggs)
- Filter tags by name !6121
- Update gitlab shell secret file also when it is empty. !3774 (glensc)
- Give project selection dropdowns responsive width, make non-wrapping.
- Fix note form hint showing slash commands supported for commits.
- Make push events have equal vertical spacing.
- API: Ensure invitees are not returned in Members API.
+ - Preserve applied filters on issues search.
- Add two-factor recovery endpoint to internal API !5510
- Pass the "Remember me" value to the U2F authentication form
- Display stages in valid order in stages dropdown on build page
- Only update projects.last_activity_at once per hour when creating a new event
+ - Cycle analytics (first iteration) !5986
- Remove vendor prefixes for linear-gradient CSS (ClemMakesApps)
- Move pushes_since_gc from the database to Redis
+ - Limit number of shown environments on Merge Request: show only environments for target_branch, source_branch and tags
- Add font color contrast to external label in admin area (ClemMakesApps)
- Change logo animation to CSS (ClemMakesApps)
- Instructions for enabling Git packfile bitmaps !6104
- Use Search::GlobalService.new in the `GET /projects/search/:query` endpoint
- Fix long comments in diffs messing with table width
+ - Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman)
- Fix pagination on user snippets page
- Run CI builds with the permissions of users !5735
- Fix sorting of issues in API
@@ -39,16 +46,20 @@ v 8.12.0 (unreleased)
- Escape search term before passing it to Regexp.new !6241 (winniehell)
- Fix pinned sidebar behavior in smaller viewports !6169
- Fix file permissions change when updating a file on the Gitlab UI !5979
+ - Added horizontal padding on build page sidebar on code coverage block. !6196 (Vitaly Baev)
- Change merge_error column from string to text type
- Reduce contributions calendar data payload (ClemMakesApps)
+ - Show all pipelines for merge requests even from discarded commits !6414
- Replace contributions calendar timezone payload with dates (ClemMakesApps)
- Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
- Enable pipeline events by default !6278
- Move parsing of sidekiq ps into helper !6245 (pascalbetz)
- Added go to issue boards keyboard shortcut
- Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel)
+ - Emoji can be awarded on Snippets !4456
- Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling)
- Fix blame table layout width
+ - Spec testing if issue authors can read issues on private projects
- Fix bug where pagination is still displayed despite all todos marked as done (ClemMakesApps)
- Request only the LDAP attributes we need !6187
- Center build stage columns in pipeline overview (ClemMakesApps)
@@ -75,6 +86,8 @@ v 8.12.0 (unreleased)
- Require confirmation when not logged in for unsubscribe links !6223 (Maximiliano Perez Coto)
- Add `wiki_page_events` to project hook APIs (Ben Boeckel)
- Remove Gitorious import
+ - Loads GFM autocomplete source only when required
+ - Fix issue with slash commands not loading on new issue page
- Fix inconsistent background color for filter input field (ClemMakesApps)
- Remove prefixes from transition CSS property (ClemMakesApps)
- Add Sentry logging to API calls
@@ -94,6 +107,7 @@ v 8.12.0 (unreleased)
- Add hover state to todos !5361 (winniehell)
- Fix icon alignment of star and fork buttons !5451 (winniehell)
- Fix alignment of icon buttons !5887 (winniehell)
+ - Added Ubuntu 16.04 support for packager.io (JonTheNiceGuy)
- Fix markdown help references (ClemMakesApps)
- Add last commit time to repo view (ClemMakesApps)
- Fix accessibility and visibility of project list dropdown button !6140
@@ -160,13 +174,11 @@ v 8.12.0 (unreleased)
- Add notification_settings API calls !5632 (mahcsig)
- Remove duplication between project builds and admin builds view !5680 (Katarzyna Kobierska Ula Budziszewska)
- Fix URLs with anchors in wiki !6300 (houqp)
- - Use a ConnectionPool for Rails.cache on Sidekiq servers
- Deleting source project with existing fork link will close all related merge requests !6177 (Katarzyna Kobierska Ula Budziszeska)
- Return 204 instead of 404 for /ci/api/v1/builds/register.json if no builds are scheduled for a runner !6225
- Fix Gitlab::Popen.popen thread-safety issue
- Add specs to removing project (Katarzyna Kobierska Ula Budziszewska)
- Clean environment variables when running git hooks
- - Add UX improvements for merge request version diffs
- Fix Import/Export issues importing protected branches and some specific models
- Fix non-master branch readme display in tree view
- Add UX improvements for merge request version diffs
diff --git a/CHANGELOG-EE b/CHANGELOG-EE
index a4be3699f5a7eed38e86caee59e65efbb34e827b..8830836bd7d60761eeb40ab77238da5fe3e3f1d1 100644
--- a/CHANGELOG-EE
+++ b/CHANGELOG-EE
@@ -1,12 +1,17 @@
Please view this file on the master branch, on stable branches it's out of date.
-v 8.12.0 (Unreleased)
+
+v 8.12.1 (unreleased)
+
+v 8.12.0
- Include more data in EE usage ping
- Reduce UPDATE queries when moving between import states on projects
- [ES] Instrument Elasticsearch::Git::Repository
- Request only the LDAP attributes we need
- Add 'Sync now' to group members page !704
+ - Add repository size limits and enforce them !740
- [ES] Instrument other Gitlab::Elastic classes
- [ES] Fix: Elasticsearch does not find partial matches in project names
+ - Faster Active Directory group membership resolution !719
- [ES] Global code search
- [ES] Improve logging
- Fix projects with remote mirrors asynchronously destruction
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 1545d966571dc86b54c98f888a0e6451501f8c81..40c341bdcdbe83bbbda981fa85368c0e1a63d0c7 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-3.5.0
+3.6.0
diff --git a/Gemfile b/Gemfile
index a4405d748b9f7a6ab2a42163f6cdb81be15bad47..656d5c107501a6d7149312be492e8e8b2dd74523 100644
--- a/Gemfile
+++ b/Gemfile
@@ -310,7 +310,7 @@ group :development, :test do
gem 'spring-commands-teaspoon', '~> 0.0.2'
gem 'rubocop', '~> 0.42.0', require: false
- gem 'rubocop-rspec', '~> 1.7.0', require: false
+ gem 'rubocop-rspec', '~> 1.5.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false
gem 'haml_lint', '~> 0.18.2', require: false
gem 'simplecov', '0.12.0', require: false
@@ -331,6 +331,7 @@ group :test do
gem 'webmock', '~> 1.21.0'
gem 'test_after_commit', '~> 0.4.2'
gem 'sham_rack', '~> 1.3.6'
+ gem 'timecop', '~> 0.8.0'
end
group :production do
diff --git a/Gemfile.lock b/Gemfile.lock
index adcbaf390684f2855ac0fcb8dafee3ef967d80f1..e893be8f9429f5b34a3249c806a1588de43723d3 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -650,8 +650,8 @@ GEM
rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
- rubocop-rspec (1.7.0)
- rubocop (>= 0.42.0)
+ rubocop-rspec (1.5.0)
+ rubocop (>= 0.40.0)
ruby-fogbugz (0.2.1)
crack (~> 0.4)
ruby-prof (0.15.9)
@@ -980,7 +980,7 @@ DEPENDENCIES
rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5)
rubocop (~> 0.42.0)
- rubocop-rspec (~> 1.7.0)
+ rubocop-rspec (~> 1.5.0)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.15.9)
sanitize (~> 2.0)
@@ -1013,6 +1013,7 @@ DEPENDENCIES
teaspoon-jasmine (~> 2.2.0)
test_after_commit (~> 0.4.2)
thin (~> 1.7.0)
+ timecop (~> 0.8.0)
turbolinks (~> 2.5.0)
u2f (~> 0.2.1)
uglifier (~> 2.7.2)
diff --git a/VERSION b/VERSION
index 434d18115f74999147ce7bfe1c40aeb7fd30b8fb..59449079c0323d4e0ecae8fbaf92ed5c844d4ad8 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.12.0-rc5-ee
+8.12.0-ee
diff --git a/app/assets/javascripts/cycle-analytics.js.es6 b/app/assets/javascripts/cycle-analytics.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..afaed7c4f60b4589e6ac17a44ff8ca4dfaa99831
--- /dev/null
+++ b/app/assets/javascripts/cycle-analytics.js.es6
@@ -0,0 +1,92 @@
+((global) => {
+
+ const COOKIE_NAME = 'cycle_analytics_help_dismissed';
+
+ gl.CycleAnalytics = class CycleAnalytics {
+ constructor() {
+ const that = this;
+
+ this.isHelpDismissed = $.cookie(COOKIE_NAME);
+ this.vue = new Vue({
+ el: '#cycle-analytics',
+ name: 'CycleAnalytics',
+ created: this.fetchData(),
+ data: this.decorateData({ isLoading: true }),
+ methods: {
+ dismissLanding() {
+ that.dismissLanding();
+ }
+ }
+ });
+ }
+
+ fetchData(options) {
+ options = options || { startDate: 30 };
+
+ $.ajax({
+ url: $('#cycle-analytics').data('request-path'),
+ method: 'GET',
+ dataType: 'json',
+ contentType: 'application/json',
+ data: { start_date: options.startDate }
+ }).done((data) => {
+ this.vue.$data = this.decorateData(data);
+ this.initDropdown();
+ })
+ .error((data) => {
+ this.handleError(data);
+ })
+ .always(() => {
+ this.vue.isLoading = false;
+ })
+ }
+
+ decorateData(data) {
+ data.summary = data.summary || [];
+ data.stats = data.stats || [];
+ data.isHelpDismissed = this.isHelpDismissed;
+ data.isLoading = data.isLoading || false;
+
+ data.summary.forEach((item) => {
+ item.value = item.value || '-';
+ });
+
+ data.stats.forEach((item) => {
+ item.value = item.value || '- - -';
+ })
+
+ return data;
+ }
+
+ handleError(data) {
+ this.vue.$data = {
+ hasError: true,
+ isHelpDismissed: this.isHelpDismissed
+ };
+
+ new Flash('There was an error while fetching cycle analytics data.', 'alert');
+ }
+
+ dismissLanding() {
+ this.vue.isHelpDismissed = true;
+ $.cookie(COOKIE_NAME, true);
+ }
+
+ initDropdown() {
+ const $dropdown = $('.js-ca-dropdown');
+ const $label = $dropdown.find('.dropdown-label');
+
+ $dropdown.find('li a').off('click').on('click', (e) => {
+ e.preventDefault();
+ const $target = $(e.currentTarget);
+ const value = $target.data('value');
+
+ $label.text($target.text().trim());
+ this.vue.isLoading = true;
+ this.fetchData({ startDate: value });
+ })
+ }
+
+ }
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index e7f29f3ed6479faafe634a91fed43cbdbe769ce1..4602f1974f623ebe763b9552a7d4c0483e07c33f 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -94,6 +94,11 @@
break;
case "projects:merge_requests:conflicts":
window.mcui = new MergeConflictResolver()
+ break;
+ case 'projects:merge_requests:index':
+ shortcut_handler = new ShortcutsNavigation();
+ Issuable.init();
+ break;
case 'dashboard:activity':
new Activities();
break;
@@ -192,6 +197,9 @@
new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList();
break;
+ case 'projects:cycle_analytics:show':
+ new gl.CycleAnalytics();
+ break;
}
switch (path.first()) {
case 'admin':
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6
index 81d89a48227f77be9fc9a0091f7a12149f384190..73e2664e9c0b08777244448e877318820957c5d1 100644
--- a/app/assets/javascripts/issuable.js.es6
+++ b/app/assets/javascripts/issuable.js.es6
@@ -15,25 +15,32 @@
return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <%- label.title %> <% }); %>');
},
initSearch: function() {
- this.timer = null;
- return $('#issuable_search').off('keyup').on('keyup', function() {
- clearTimeout(this.timer);
- return this.timer = setTimeout(function() {
- var $form, $input, $search;
- $search = $('#issuable_search');
- $form = $('.js-filter-form');
- $input = $("input[name='" + ($search.attr('name')) + "']", $form);
- if ($input.length === 0) {
- $form.append("");
- } else {
- $input.val($search.val());
- }
- if ($search.val() !== '') {
- return Issuable.filterResults($form);
- }
- }, 500);
+ // `immediate` param set to false debounces on the `trailing` edge, lets user finish typing
+ const debouncedExecSearch = _.debounce(Issuable.executeSearch, 500, false);
+
+ $('#issuable_search').off('keyup').on('keyup', debouncedExecSearch);
+
+ // ensures existing filters are preserved when manually submitted
+ $('#issue_search_form').on('submit', (e) => {
+ e.preventDefault();
+ debouncedExecSearch(e);
});
},
+ executeSearch: function(e) {
+ const $search = $('#issuable_search');
+ const $searchName = $search.attr('name');
+ const $searchValue = $search.val();
+ const $filtersForm = $('.js-filter-form');
+ const $input = $(`input[name='${$searchName}']`, $filtersForm);
+
+ if (!$input.length) {
+ $filtersForm.append(``);
+ } else {
+ $input.val($searchValue);
+ }
+
+ Issuable.filterResults($filtersForm);
+ },
initLabelFilterRemove: function() {
return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) {
var $button;
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 553768b2e68de0585a5c498a3aa1446f7e455f9b..ea43f4afc374ba176dc1ee40c838087df23dd35c 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -99,8 +99,7 @@
.top-area {
@include clearfix;
-
- border-bottom: 1px solid #eee;
+ border-bottom: 1px solid $btn-gray-hover;
.nav-text {
padding-top: 16px;
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index c879074c7fee48ffebff65531eb8540d16b9501d..a5a260d4c8fc7f728eb91edd4cdeaa0d41a1c670 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -109,6 +109,10 @@
width: 100%;
}
+ .block-first {
+ padding: 5px 16px 11px;
+ }
+
.js-build-variable {
color: $code-color;
}
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
new file mode 100644
index 0000000000000000000000000000000000000000..21e19c9763282f080b2349efc62342c7074c22b1
--- /dev/null
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -0,0 +1,121 @@
+#cycle-analytics {
+ margin: 24px auto 0;
+ width: 800px;
+ position: relative;
+
+ .panel {
+
+ .content-block {
+ padding: 24px 0;
+ border-bottom: none;
+ position: relative;
+ }
+
+ .column {
+ text-align: center;
+
+ .header {
+ font-size: 30px;
+ line-height: 38px;
+ font-weight: normal;
+ margin: 0;
+ }
+
+ .text {
+ color: $layout-link-gray;
+ margin: 0;
+ }
+
+ &:last-child {
+ text-align: right;
+ }
+ }
+
+ .dropdown {
+ position: relative;
+ top: 13px;
+ }
+ }
+
+ .bordered-box {
+ border: 1px solid $border-color;
+ @include border-radius($border-radius-default);
+ position: relative;
+ }
+
+ .content-list {
+ li {
+ padding: 18px $gl-padding $gl-padding;
+
+ .container-fluid {
+ padding: 0;
+ }
+ }
+
+ .title-col {
+ p {
+ margin: 0;
+
+ &.title {
+ line-height: 19px;
+ font-size: 15px;
+ font-weight: 600;
+ }
+ &:text {
+ color: #8c8c8c;
+ }
+ }
+ }
+
+ .value-col {
+ text-align: right;
+
+ span {
+ line-height: 42px;
+ }
+ }
+ }
+
+ .landing {
+ margin-bottom: $gl-padding;
+ overflow: hidden;
+
+ .dismiss-icon {
+ position: absolute;
+ right: $gl-padding;
+ cursor: pointer;
+ color: #b2b2b2;
+ }
+
+ svg {
+ margin: 0 20px;
+ float: left;
+ width: 136px;
+ height: 136px;
+ }
+
+ .inner-content {
+ width: 480px;
+ float: left;
+
+ h4 {
+ color: $gl-text-color;
+ font-size: 17px;
+ }
+
+ p {
+ color: #8c8c8c;
+ margin-bottom: $gl-padding;
+ }
+ }
+ }
+
+ .fa-spinner {
+ font-size: 28px;
+ position: relative;
+ margin-left: -20px;
+ left: 50%;
+ margin-top: 36px;
+ }
+
+}
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 73f1474b45f37ba6ee1ca8b23dab5b23097daff5..ec7871376c162846fcfb50f9e1bef1e35ab95313 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -66,3 +66,16 @@
margin-bottom: $gl-padding;
}
}
+
+.groups-header {
+
+ @media (min-width: $screen-sm-min) {
+ .nav-links {
+ width: 35%;
+ }
+
+ .nav-controls {
+ width: 65%;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss
index 5270aea4e797f3f4212b5ead3e6518008773b9e9..4d5df566d9bf57554c50a7105ec9e1ffc3cc0cd9 100644
--- a/app/assets/stylesheets/pages/snippets.scss
+++ b/app/assets/stylesheets/pages/snippets.scss
@@ -12,11 +12,18 @@
.snippet-file-content {
border-radius: 3px;
+ margin-bottom: $gl-padding;
+
.btn-clipboard {
@extend .btn;
}
}
+.project-snippets .awards {
+ border-bottom: 1px solid $table-border-color;
+ padding-bottom: $gl-padding;
+}
+
.snippet-title {
font-size: 24px;
font-weight: 600;
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 3aaa4fee4b136dc24445d7ef34737d5a4e895ad9..6397ffd5c5a41b005f238c2543a13ab49d4a8153 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -134,6 +134,7 @@ def application_setting_params
:usage_ping_enabled,
:repository_storage,
:enabled_git_access_protocol,
+ :repository_size_limit,
restricted_visibility_levels: [],
import_sources: [],
disabled_oauth_sign_in_sources: []
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index aed77d0358a7b90cbd8335afe12e80046d0bc1e8..8b7a40e4a17ede72bb6345aa8eff31926b788f02 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -66,6 +66,7 @@ def group_params
:lfs_enabled,
:name,
:path,
+ :repository_size_limit,
:request_access_enabled,
:visibility_level
)
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 4a447735fa7625d39415876b463ea2c63b4d4563..b5e79099e39590d94c114ebb81e78d4a1fccc38a 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -13,18 +13,10 @@ def issues_collection
issues_finder.execute
end
- def all_issues_collection
- IssuesFinder.new(current_user, filter_params_all).execute
- end
-
def merge_requests_collection
merge_requests_finder.execute
end
- def all_merge_requests_collection
- MergeRequestsFinder.new(current_user, filter_params_all).execute
- end
-
def issues_finder
@issues_finder ||= issuable_finder_for(IssuesFinder)
end
@@ -62,10 +54,6 @@ def filter_params
@filter_params
end
- def filter_params_all
- @filter_params_all ||= filter_params.merge(state: 'all', sort: nil)
- end
-
def set_default_scope
params[:scope] = 'all' if params[:scope].blank?
end
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index eced9d9d6784a390635b5f3385bc26230bb877c6..b89fb94be6ea7f6df2185d72573d4ea333192e4d 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -10,8 +10,6 @@ def issues
.preload(:author, :project)
.page(params[:page])
- @all_issues = all_issues_collection.non_archived
-
respond_to do |format|
format.html
format.atom { render layout: false }
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index 729763169e28e8da6619b4d3d26470e44a83a649..a1b0eee37f91a5131e8002045c09d1f21c5ffff3 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -9,7 +9,5 @@ def merge_requests
.non_archived
.preload(:author, :target_project)
.page(params[:page])
-
- @all_merge_requests = all_merge_requests_collection.non_archived
end
end
diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb
index 172d5344b7a1594bd096b6291616f2d728451eb9..3717c49f272f4d91f704267e5bc621e0c2adb79f 100644
--- a/app/controllers/concerns/toggle_award_emoji.rb
+++ b/app/controllers/concerns/toggle_award_emoji.rb
@@ -10,7 +10,9 @@ def toggle_award_emoji
if awardable.user_can_award?(current_user, name)
awardable.toggle_award_emoji(name, current_user)
- TodoService.new.new_award_emoji(to_todoable(awardable), current_user)
+
+ todoable = to_todoable(awardable)
+ TodoService.new.new_award_emoji(todoable, current_user) if todoable
render json: { ok: true }
else
@@ -24,8 +26,10 @@ def to_todoable(awardable)
case awardable
when Note
awardable.noteable
- else
+ when MergeRequest, Issue
awardable
+ when Snippet
+ nil
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 0184e38373eb4fe5a1c6788973c46e578e093abd..b39c3c21ad5d30f76aa634a71667d11149c050c3 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -131,12 +131,13 @@ def group_params
:avatar,
:description,
:lfs_enabled,
+ :membership_lock,
:name,
:path,
:public,
+ :repository_size_limit,
:request_access_enabled,
:share_with_group_lock,
- :membership_lock,
:visibility_level
)
end
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 06d967747545e30b43ffb3232c8c8c7d8c2a1dd3..34d5d99558e8c082327718feef8db12f162bdbbd 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -11,10 +11,8 @@ def auth
service = SERVICES[params[:service]]
return head :not_found unless service
- @authentication_result ||= Gitlab::Auth::Result.new
-
result = service.new(@authentication_result.project, @authentication_result.actor, auth_params).
- execute(authentication_abilities: @authentication_result.authentication_abilities)
+ execute(authentication_abilities: @authentication_result.authentication_abilities || [])
render json: result, status: result[:http_status]
end
@@ -22,6 +20,8 @@ def auth
private
def authenticate_project_or_user
+ @authentication_result = Gitlab::Auth::Result.new
+
authenticate_with_http_basic do |login, password|
@authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..16a7b1fc6e26618ba634279476c9bfdf03012ba2
--- /dev/null
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -0,0 +1,67 @@
+class Projects::CycleAnalyticsController < Projects::ApplicationController
+ include ActionView::Helpers::DateHelper
+ include ActionView::Helpers::TextHelper
+
+ before_action :authorize_read_cycle_analytics!
+
+ def show
+ @cycle_analytics = CycleAnalytics.new(@project, from: parse_start_date)
+
+ respond_to do |format|
+ format.html
+ format.json { render json: cycle_analytics_json }
+ end
+ end
+
+ private
+
+ def parse_start_date
+ case cycle_analytics_params[:start_date]
+ when '30' then 30.days.ago
+ when '90' then 90.days.ago
+ else 90.days.ago
+ end
+ end
+
+ def cycle_analytics_params
+ return {} unless params[:cycle_analytics].present?
+
+ { start_date: params[:cycle_analytics][:start_date] }
+ end
+
+ def cycle_analytics_json
+ cycle_analytics_view_data = [[:issue, "Issue", "Time before an issue gets scheduled"],
+ [:plan, "Plan", "Time before an issue starts implementation"],
+ [:code, "Code", "Time until first merge request"],
+ [:test, "Test", "Total test time for all commits/merges"],
+ [:review, "Review", "Time between merge request creation and merge/close"],
+ [:staging, "Staging", "From merge request merge until deploy to production"],
+ [:production, "Production", "From issue creation until deploy to production"]]
+
+ stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_description)|
+ value = @cycle_analytics.send(stage_method).presence
+
+ stats << {
+ title: stage_text,
+ description: stage_description,
+ value: value && !value.zero? ? distance_of_time_in_words(value) : nil
+ }
+ stats
+ end
+
+ issues = @cycle_analytics.summary.new_issues
+ commits = @cycle_analytics.summary.commits
+ deploys = @cycle_analytics.summary.deploys
+
+ summary = [
+ { title: "New Issue".pluralize(issues), value: issues },
+ { title: "Commit".pluralize(commits), value: commits },
+ { title: "Deploy".pluralize(deploys), value: deploys }
+ ]
+
+ {
+ summary: summary,
+ stats: stats
+ }
+ end
+end
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index cbfd3cab3dd8f731b715cd8c5a479b948b975074..383e184d7965aa84cf84d36f7ae2e73bfe87c5fa 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -32,11 +32,11 @@ def authenticate_user
return # Allow access
end
elsif allow_kerberos_spnego_auth? && spnego_provided?
- user = find_kerberos_user
+ kerberos_user = find_kerberos_user
- if user
+ if kerberos_user
@authentication_result = Gitlab::Auth::Result.new(
- user, nil, :kerberos, Gitlab::Auth.full_authentication_abilities)
+ kerberos_user, nil, :kerberos, Gitlab::Auth.full_authentication_abilities)
send_final_spnego_response
return # Allow access
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index 662d38b10a5867f49c1fe3c76e6425df8ad4ede5..785de7e2123f3e90851debd2687c56139061b17c 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -68,7 +68,9 @@ def render_http_not_allowed
def render_denied
if user && user.can?(:read_project, project)
- render plain: 'Access denied', status: :forbidden
+ message = project.above_size_limit? ? access_check.message : 'Access denied'
+
+ render plain: message, status: :forbidden
else
# Do not leak information about project existence
render_not_found
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 2d490eb14a3ec69e35e254e09dadd97d1affab5b..bdd4ad9712b5473788147ff87353df321e620a7d 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -28,8 +28,6 @@ def index
@labels = @project.labels.where(title: params[:label_name])
- @all_issues = all_issues_collection
-
respond_to do |format|
format.html
format.atom { render layout: false }
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 38bd2cf6791aaf8cce6a5f7914c6c42cdb732029..883e4413eaae0abffacc2deb100addb39819e3a3 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -38,8 +38,6 @@ def index
@labels = @project.labels.where(title: params[:label_name])
- @all_merge_requests = all_merge_requests_collection
-
respond_to do |format|
format.html
format.json do
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 17ceefec3b86ff45b2e663ae5698cb845b84a534..e290a0eadda814488d8a3893abbe7f43e3a87f0b 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -1,6 +1,8 @@
class Projects::SnippetsController < Projects::ApplicationController
+ include ToggleAwardEmoji
+
before_action :module_enabled
- before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
+ before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji]
# Allow read any snippet
before_action :authorize_read_project_snippet!, except: [:new, :create, :index]
@@ -80,6 +82,7 @@ def raw
def snippet
@snippet ||= @project.snippets.find(params[:id])
end
+ alias_method :awardable, :snippet
def authorize_read_project_snippet!
return render_404 unless can?(current_user, :read_project_snippet, @snippet)
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 16938972279adcb4a6cde6f61945e14d3090f0cc..d1404d66afc6a60333f65b42767ce58bb15c6e58 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -331,6 +331,7 @@ def project_params
:mirror,
:mirror_user_id,
:mirror_trigger_builds,
+ :repository_size_limit,
:reset_approvals_on_push
)
end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 2a17c1f34db2825e9fb35a809fc91fdbef114da1..d198782138a380f52cb54bcf58f85546b722b76c 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -1,4 +1,6 @@
class SnippetsController < ApplicationController
+ include ToggleAwardEmoji
+
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read snippet
@@ -85,6 +87,7 @@ def snippet
PersonalSnippet.find(params[:id])
end
end
+ alias_method :awardable, :snippet
def authorize_read_snippet!
authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet)
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 0a99ce7331290ce5ce6bd2435f6e953684ece4fc..8f6537086b9669ae0b9aaabf0ca94029ae15a197 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -283,14 +283,23 @@ def path_to_key(key, admin = false)
end
end
- def state_filters_text_for(state, records)
+ def state_filters_text_for(entity, project)
titles = {
opened: "Open"
}
- state_title = titles[state] || state.to_s.humanize
- count = records.public_send(state).size
- html = content_tag :span, state_title
+ entity_title = titles[entity] || entity.to_s.humanize
+
+ count =
+ if project.nil?
+ nil
+ elsif current_controller?(:issues)
+ project.issues.visible_to_user(current_user).send(entity).count
+ elsif current_controller?(:merge_requests)
+ project.merge_requests.send(entity).count
+ end
+
+ html = content_tag :span, entity_title
if count.present?
html += " "
diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..aa134cea31c6ebbab9278b5ac65bc23d0a05511c
--- /dev/null
+++ b/app/helpers/award_emoji_helper.rb
@@ -0,0 +1,9 @@
+module AwardEmojiHelper
+ def toggle_award_url(awardable)
+ if @project
+ url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
+ else
+ url_for([:toggle_award_emoji, awardable])
+ end
+ end
+end
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index a322a90cc4e037aeb3f90b4e36c6eea3f30ea6c4..670a7ca36f48f586faef7d14f34ed15b4901d5c7 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -46,6 +46,10 @@ def project_environments_path(project, *args)
namespace_project_environments_path(project.namespace, project, *args)
end
+ def project_cycle_analytics_path(project, *args)
+ namespace_project_cycle_analytics_path(project.namespace, project, *args)
+ end
+
def project_builds_path(project, *args)
namespace_project_builds_path(project.namespace, project, *args)
end
@@ -66,6 +70,10 @@ def runner_path(runner, *args)
namespace_project_runner_path(@project.namespace, @project, runner, *args)
end
+ def environment_path(environment, *args)
+ namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args)
+ end
+
def issue_path(entity, *args)
namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args)
end
@@ -98,6 +106,14 @@ def toggle_subscription_path(entity, *args)
end
end
+ def toggle_award_emoji_personal_snippet_path(*args)
+ toggle_award_emoji_snippet_path(*args)
+ end
+
+ def toggle_award_emoji_namespace_project_project_snippet_path(*args)
+ toggle_award_emoji_namespace_project_snippet_path(*args)
+ end
+
## Members
def project_members_url(project, *args)
namespace_project_project_members_url(project.namespace, project)
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index ab880ed6de0874ab773b016da8eb31c70bb80300..9c8ee69e592ae5b7e629bd6dcc176df6dbe23bba 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -41,6 +41,12 @@ def projects_lfs_status(group)
end
end
+ def size_limit_message_for_group(group)
+ show_lfs = group.lfs_enabled? ? 'and their respective LFS files' : ''
+
+ "Repositories within this group #{show_lfs} will be restricted to this maximum size. Can be overridden inside each project. 0 for unlimited. Leave empty to inherit the global value."
+ end
+
def group_lfs_status(group)
status = group.lfs_enabled? ? 'enabled' : 'disabled'
diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb
index c15ecc8f86eeabb6a768b2ad975f68f840edb6db..4d2a29ecb1d74e000c2eacebdd7b7fa6bc587386 100644
--- a/app/helpers/lfs_helper.rb
+++ b/app/helpers/lfs_helper.rb
@@ -1,11 +1,13 @@
module LfsHelper
+ include Gitlab::Routing.url_helpers
+
def require_lfs_enabled!
return if Gitlab.config.lfs.enabled
render(
json: {
message: 'Git LFS is not enabled on this GitLab server, contact your admin.',
- documentation_url: "#{Gitlab.config.gitlab.url}/help",
+ documentation_url: help_url,
},
status: 501
)
@@ -16,7 +18,11 @@ def lfs_check_access!
return if upload_request? && lfs_upload_access?
if project.public? || (user && user.can?(:read_project, project))
- render_lfs_forbidden
+ if project.above_size_limit? || objects_exceed_repo_limit?
+ render_size_error
+ else
+ render_lfs_forbidden
+ end
else
render_lfs_not_found
end
@@ -38,15 +44,25 @@ def build_can_download_code?
def lfs_upload_access?
return false unless project.lfs_enabled?
+ return false if project.above_size_limit? || objects_exceed_repo_limit?
has_authentication_ability?(:push_code) && can?(user, :push_code, project)
end
+ def objects_exceed_repo_limit?
+ return false unless project.size_limit_enabled?
+ return @limit_exceeded if defined?(@limit_exceeded)
+
+ size_of_objects = objects.sum { |o| o[:size] }
+
+ @limit_exceeded = (project.repository_and_lfs_size + size_of_objects.to_mb) > project.actual_size_limit
+ end
+
def render_lfs_forbidden
render(
json: {
message: 'Access forbidden. Check your access level.',
- documentation_url: "#{Gitlab.config.gitlab.url}/help",
+ documentation_url: help_url,
},
content_type: "application/vnd.git-lfs+json",
status: 403
@@ -57,13 +73,24 @@ def render_lfs_not_found
render(
json: {
message: 'Not found.',
- documentation_url: "#{Gitlab.config.gitlab.url}/help",
+ documentation_url: help_url,
},
content_type: "application/vnd.git-lfs+json",
status: 404
)
end
+ def render_size_error
+ render(
+ json: {
+ message: Gitlab::RepositorySizeError.new(project).push_error,
+ documentation_url: help_url,
+ },
+ content_type: "application/vnd.git-lfs+json",
+ status: 406
+ )
+ end
+
def storage_project
@storage_project ||= begin
result = project
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 8d7a4b80471ce01e86cfa2e138da177e517e4f13..235f8d8ad88a5bfed6a8cfb9fc3f5016ad4331a3 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -214,6 +214,12 @@ def project_lfs_status(project)
end
end
+ def size_limit_message(project)
+ show_lfs = project.lfs_enabled? ? 'including files in LFS' : ''
+
+ "The total size of this project's repository #{show_lfs} will be limited to this size. 0 for unlimited. Leave empty to inherit the group/global value."
+ end
+
def git_user_name
if current_user
current_user.name
@@ -231,8 +237,12 @@ def git_user_email
end
def repository_size(project = @project)
- size_in_bytes = project.repository_size * 1.megabyte
- number_to_human_size(size_in_bytes, delimiter: ',', precision: 2)
+ size_in_bytes = project.repository_and_lfs_size * 1.megabyte
+ limit_in_bytes = project.actual_size_limit * 1.megabyte
+
+ limit_text = limit_in_bytes.zero? ? '' : "/#{number_to_human_size(limit_in_bytes, delimiter: ',', precision: 2)}"
+
+ "#{number_to_human_size(size_in_bytes, delimiter: ',', precision: 2)}#{limit_text}"
end
def default_url_to_repo(project = @project)
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 24a33060dfbf070ba42004731229ffa33c449b97..da0fb09b62f5063ee29b41f2e9826da42ec573e7 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -63,6 +63,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
numericality: { only_integer: true, greater_than: 0 }
+ validates :repository_size_limit,
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
validates :container_registry_token_expire_delay,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index f86806f0eb99a157379d1aa738dddda20f1f0683..f268aa9e0fee0ef86c9726a193fcc060d0a0c7e6 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -494,8 +494,11 @@ def build_attributes_from_config
end
def hide_secrets(trace)
- trace = Ci::MaskSecret.mask(trace, project.runners_token) if project
- trace = Ci::MaskSecret.mask(trace, token)
+ return unless trace
+
+ trace = trace.dup
+ Ci::MaskSecret.mask!(trace, project.runners_token) if project
+ Ci::MaskSecret.mask!(trace, token)
trace
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 895eac1a258b597fcc7e9a6a707a46e537c8c1cf..663c5b1e2315a3def61cb9672786eb2cdae8f490 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -56,6 +56,16 @@ class Pipeline < ActiveRecord::Base
pipeline.finished_at = Time.now
end
+ after_transition [:created, :pending] => :running do |pipeline|
+ MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)).
+ update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: nil)
+ end
+
+ after_transition any => [:success] do |pipeline|
+ MergeRequest::Metrics.where(merge_request_id: pipeline.merge_requests.map(&:id)).
+ update_all(latest_build_finished_at: pipeline.finished_at)
+ end
+
before_transition do |pipeline|
pipeline.update_duration
end
@@ -280,6 +290,16 @@ def execute_hooks
project.execute_services(data, :pipeline_hooks)
end
+ # Merge requests for which the current pipeline is running against
+ # the merge request's latest commit.
+ def merge_requests
+ @merge_requests ||=
+ begin
+ project.merge_requests.where(source_branch: self.ref).
+ select { |merge_request| merge_request.pipeline.try(:id) == self.id }
+ end
+ end
+
private
def pipeline_data
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index d8d4575bb4dff230aa4259b99df580084b68dc35..073ac4c1b65ef43ae2fb8ad8711aad8f629b87c9 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -71,6 +71,12 @@ def user_can_award?(current_user, name)
end
end
+ def user_authored?(current_user)
+ author = self.respond_to?(:author) ? self.author : self.user
+
+ author == current_user
+ end
+
def awarded_emoji?(emoji_name, current_user)
award_emoji.where(name: emoji_name, user: current_user).exists?
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index db519d2b7d42fdb01ccd1d0cb69e3680b193dc58..1e53cbdbd261278fb725fba5684d8c480935c395 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -28,10 +28,13 @@ def award_emojis_loaded?
loaded? && to_a.all? { |note| note.association(:award_emoji).loaded? }
end
end
+
has_many :label_links, as: :target, dependent: :destroy
has_many :labels, through: :label_links
has_many :todos, as: :target, dependent: :destroy
+ has_one :metrics
+
validates :author, presence: true
validates :title, presence: true, length: { within: 0..255 }
@@ -84,6 +87,7 @@ def award_emojis_loaded?
acts_as_paranoid
after_save :update_assignee_cache_counts, if: :assignee_id_changed?
+ after_save :record_metrics
def update_assignee_cache_counts
# make sure we flush the cache for both the old *and* new assignee
@@ -199,10 +203,6 @@ def user_notes_count
end
end
- def user_authored?(user)
- user == author
- end
-
def subscribed_without_subscriptions?(user)
participants(user).include?(user)
end
@@ -289,4 +289,9 @@ def updated_tasks
def can_move?(*)
false
end
+
+ def record_metrics
+ metrics = self.metrics || create_metrics
+ metrics.record!
+ end
end
diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb
new file mode 100644
index 0000000000000000000000000000000000000000..be295487fd2786c0e47f4157e86ef704993aac5c
--- /dev/null
+++ b/app/models/cycle_analytics.rb
@@ -0,0 +1,97 @@
+class CycleAnalytics
+ include Gitlab::Database::Median
+ include Gitlab::Database::DateTime
+
+ def initialize(project, from:)
+ @project = project
+ @from = from
+ end
+
+ def summary
+ @summary ||= Summary.new(@project, from: @from)
+ end
+
+ def issue
+ calculate_metric(:issue,
+ Issue.arel_table[:created_at],
+ [Issue::Metrics.arel_table[:first_associated_with_milestone_at],
+ Issue::Metrics.arel_table[:first_added_to_board_at]])
+ end
+
+ def plan
+ calculate_metric(:plan,
+ [Issue::Metrics.arel_table[:first_associated_with_milestone_at],
+ Issue::Metrics.arel_table[:first_added_to_board_at]],
+ Issue::Metrics.arel_table[:first_mentioned_in_commit_at])
+ end
+
+ def code
+ calculate_metric(:code,
+ Issue::Metrics.arel_table[:first_mentioned_in_commit_at],
+ MergeRequest.arel_table[:created_at])
+ end
+
+ def test
+ calculate_metric(:test,
+ MergeRequest::Metrics.arel_table[:latest_build_started_at],
+ MergeRequest::Metrics.arel_table[:latest_build_finished_at])
+ end
+
+ def review
+ calculate_metric(:review,
+ MergeRequest.arel_table[:created_at],
+ MergeRequest::Metrics.arel_table[:merged_at])
+ end
+
+ def staging
+ calculate_metric(:staging,
+ MergeRequest::Metrics.arel_table[:merged_at],
+ MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
+ end
+
+ def production
+ calculate_metric(:production,
+ Issue.arel_table[:created_at],
+ MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
+ end
+
+ private
+
+ def calculate_metric(name, start_time_attrs, end_time_attrs)
+ cte_table = Arel::Table.new("cte_table_for_#{name}")
+
+ # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
+ # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
+ # We compute the (end_time - start_time) interval, and give it an alias based on the current
+ # cycle analytics stage.
+ interval_query = Arel::Nodes::As.new(
+ cte_table,
+ subtract_datetimes(base_query, end_time_attrs, start_time_attrs, name.to_s))
+
+ median_datetime(cte_table, interval_query, name)
+ end
+
+ # Join table with a row for every pair (where the merge request
+ # closes the given issue) with issue and merge request metrics included. The metrics
+ # are loaded with an inner join, so issues / merge requests without metrics are
+ # automatically excluded.
+ def base_query
+ arel_table = MergeRequestsClosingIssues.arel_table
+
+ # Load issues
+ query = arel_table.join(Issue.arel_table).on(Issue.arel_table[:id].eq(arel_table[:issue_id])).
+ join(Issue::Metrics.arel_table).on(Issue.arel_table[:id].eq(Issue::Metrics.arel_table[:issue_id])).
+ where(Issue.arel_table[:project_id].eq(@project.id)).
+ where(Issue.arel_table[:deleted_at].eq(nil)).
+ where(Issue.arel_table[:created_at].gteq(@from))
+
+ # Load merge_requests
+ query = query.join(MergeRequest.arel_table, Arel::Nodes::OuterJoin).
+ on(MergeRequest.arel_table[:id].eq(arel_table[:merge_request_id])).
+ join(MergeRequest::Metrics.arel_table).
+ on(MergeRequest.arel_table[:id].eq(MergeRequest::Metrics.arel_table[:merge_request_id]))
+
+ # Limit to merge requests that have been deployed to production after `@from`
+ query.where(MergeRequest::Metrics.arel_table[:first_deployed_to_production_at].gteq(@from))
+ end
+end
diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb
new file mode 100644
index 0000000000000000000000000000000000000000..53b2cacb131f9e8fbdfc2d008892c309e1dcc4c1
--- /dev/null
+++ b/app/models/cycle_analytics/summary.rb
@@ -0,0 +1,24 @@
+class CycleAnalytics
+ class Summary
+ def initialize(project, from:)
+ @project = project
+ @from = from
+ end
+
+ def new_issues
+ @project.issues.created_after(@from).count
+ end
+
+ def commits
+ repository = @project.repository.raw_repository
+
+ if @project.default_branch
+ repository.log(ref: @project.default_branch, after: @from).count
+ end
+ end
+
+ def deploys
+ @project.deployments.where("created_at > ?", @from).count
+ end
+ end
+end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 1e338889714ccd5b69980fc5ad57a725258a0b5c..07d7e19e70d89813e521ba8e3dccc47eeb2c1b11 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -42,4 +42,38 @@ def includes_commit?(commit)
project.repository.is_ancestor?(commit.id, sha)
end
+
+ def update_merge_request_metrics!
+ return unless environment.update_merge_request_metrics?
+
+ merge_requests = project.merge_requests.
+ joins(:metrics).
+ where(target_branch: self.ref, merge_request_metrics: { first_deployed_to_production_at: nil }).
+ where("merge_request_metrics.merged_at <= ?", self.created_at)
+
+ if previous_deployment
+ merge_requests = merge_requests.where("merge_request_metrics.merged_at >= ?", previous_deployment.created_at)
+ end
+
+ # Need to use `map` instead of `select` because MySQL doesn't allow `SELECT`ing from the same table
+ # that we're updating.
+ merge_request_ids =
+ if Gitlab::Database.postgresql?
+ merge_requests.select(:id)
+ elsif Gitlab::Database.mysql?
+ merge_requests.map(&:id)
+ end
+
+ MergeRequest::Metrics.
+ where(merge_request_id: merge_request_ids, first_deployed_to_production_at: nil).
+ update_all(first_deployed_to_production_at: self.created_at)
+ end
+
+ def previous_deployment
+ @previous_deployment ||=
+ project.deployments.joins(:environment).
+ where(environments: { name: self.environment.name }, ref: self.ref).
+ where.not(id: self.id).
+ take
+ end
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 33c9abf382a1666a2a16ef5645d0ff52fed08519..49e0a20640ce6632e2ecf5fd56453343213c6b20 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -43,4 +43,8 @@ def includes_commit?(commit)
last_deployment.includes_commit?(commit)
end
+
+ def update_merge_request_metrics?
+ self.name == "production"
+ end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 11b09a92783759e3dbdb9b1375acd9c2063518c1..59718668a5296f83457c060ceadd5579cb94b1a2 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -33,6 +33,9 @@ class Group < Namespace
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
+ validates :repository_size_limit,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true }
+
mount_uploader :avatar, AvatarUploader
after_create :post_create_hook
@@ -199,6 +202,12 @@ def post_destroy_hook
system_hook_service.execute_hooks_for(self, :destroy)
end
+ def actual_size_limit
+ return current_application_settings.repository_size_limit if repository_size_limit.nil?
+
+ repository_size_limit
+ end
+
def system_hook_service
SystemHooksService.new
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 5e2a39dc51006e40e8c65b5d7e866e6622b1b1a2..b36881e08632c5b0b8aa6000bb3cb7212769a3f2 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -29,6 +29,8 @@ class Issue < ActiveRecord::Base
has_many :events, as: :target, dependent: :destroy
+ has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
+
validates :project, presence: true
scope :cared, ->(user) { where(assignee_id: user) }
@@ -44,6 +46,8 @@ class Issue < ActiveRecord::Base
scope :order_weight_desc, -> { reorder('weight IS NOT NULL, weight DESC') }
scope :order_weight_asc, -> { reorder('weight ASC') }
+ scope :created_after, -> (datetime) { where("created_at >= ?", datetime) }
+
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
diff --git a/app/models/issue/metrics.rb b/app/models/issue/metrics.rb
new file mode 100644
index 0000000000000000000000000000000000000000..012d545c44093baaf457732c236428ac9f4055f0
--- /dev/null
+++ b/app/models/issue/metrics.rb
@@ -0,0 +1,21 @@
+class Issue::Metrics < ActiveRecord::Base
+ belongs_to :issue
+
+ def record!
+ if issue.milestone_id.present? && self.first_associated_with_milestone_at.blank?
+ self.first_associated_with_milestone_at = Time.now
+ end
+
+ if issue_assigned_to_list_label? && self.first_added_to_board_at.blank?
+ self.first_added_to_board_at = Time.now
+ end
+
+ self.save
+ end
+
+ private
+
+ def issue_assigned_to_list_label?
+ issue.labels.any? { |label| label.lists.present? }
+ end
+end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index a26a80f786360dca332ac32cd59de92e7501bbef..d811f8624fe75e8f5b50a0ca3409f3f8b6c191a5 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -19,6 +19,8 @@ class MergeRequest < ActiveRecord::Base
has_many :events, as: :target, dependent: :destroy
+ has_many :merge_requests_closing_issues, class_name: 'MergeRequestsClosingIssues', dependent: :delete_all
+
serialize :merge_params, Hash
after_create :ensure_merge_request_diff, unless: :importing?
@@ -522,6 +524,19 @@ def project
target_project
end
+ # If the merge request closes any issues, save this information in the
+ # `MergeRequestsClosingIssues` model. This is a performance optimization.
+ # Calculating this information for a number of merge requests requires
+ # running `ReferenceExtractor` on each of them separately.
+ def cache_merge_request_closes_issues!(current_user = self.author)
+ transaction do
+ self.merge_requests_closing_issues.delete_all
+ closes_issues(current_user).each do |issue|
+ self.merge_requests_closing_issues.create!(issue: issue)
+ end
+ end
+ end
+
def closes_issue?(issue)
closes_issues.include?(issue)
end
@@ -529,7 +544,8 @@ def closes_issue?(issue)
# Return the set of issues that will be closed if this merge request is accepted.
def closes_issues(current_user = self.author)
if target_branch == project.default_branch
- messages = commits.map(&:safe_message) << description
+ messages = [description]
+ messages.concat(commits.map(&:safe_message)) if merge_request_diff
Gitlab::ClosingIssueExtractor.new(project, current_user).
closed_by_message(messages.join("\n"))
@@ -771,9 +787,12 @@ def mergeable_ci_state?
def environments
return [] unless diff_head_commit
- target_project.environments.select do |environment|
- environment.includes_commit?(diff_head_commit)
- end
+ environments = source_project.environments_for(
+ source_branch, diff_head_commit)
+ environments += target_project.environments_for(
+ target_branch, diff_head_commit, with_tags: true)
+
+ environments.uniq
end
def state_human_name
@@ -878,10 +897,23 @@ def pipeline
end
def all_pipelines
- @all_pipelines ||=
- if diff_head_sha && source_project
- source_project.pipelines.order(id: :desc).where(sha: commits_sha, ref: source_branch)
- end
+ return unless source_project
+
+ @all_pipelines ||= begin
+ sha = if persisted?
+ all_commits_sha
+ else
+ diff_head_sha
+ end
+
+ source_project.pipelines.order(id: :desc).
+ where(sha: sha, ref: source_branch)
+ end
+ end
+
+ # Note that this could also return SHA from now dangling commits
+ def all_commits_sha
+ merge_request_diffs.flat_map(&:commits_sha).uniq
end
def merge_commit
diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb
new file mode 100644
index 0000000000000000000000000000000000000000..99c49a020c974ef3d2e1873886b11cbb21e2831c
--- /dev/null
+++ b/app/models/merge_request/metrics.rb
@@ -0,0 +1,11 @@
+class MergeRequest::Metrics < ActiveRecord::Base
+ belongs_to :merge_request
+
+ def record!
+ if merge_request.merged? && self.merged_at.blank?
+ self.merged_at = Time.now
+ end
+
+ self.save
+ end
+end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 18c583add8848174d2e250dddb667dc54f326f18..36b8b70870bc799904a1c779339e075ca06cd5cb 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -30,6 +30,10 @@ def self.select_without_diff
select(column_names - ['st_diffs'])
end
+ def st_commits
+ super || []
+ end
+
# Collect information about commits and diff from repository
# and save it to the database as serialized data
def save_git_content
@@ -83,7 +87,7 @@ def raw_diffs(options = {})
end
def commits
- @commits ||= load_commits(st_commits || [])
+ @commits ||= load_commits(st_commits)
end
def reload_commits
@@ -117,6 +121,14 @@ def head_commit
project.commit(head_commit_sha)
end
+ def commits_sha
+ if @commits
+ commits.map(&:sha)
+ else
+ st_commits.map { |commit| commit[:id] }
+ end
+ end
+
def diff_refs
return unless start_commit_sha || base_commit_sha
diff --git a/app/models/merge_requests_closing_issues.rb b/app/models/merge_requests_closing_issues.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ab597c379471afa8becb49d0bcfaaa78137b8239
--- /dev/null
+++ b/app/models/merge_requests_closing_issues.rb
@@ -0,0 +1,7 @@
+class MergeRequestsClosingIssues < ActiveRecord::Base
+ belongs_to :merge_request
+ belongs_to :issue
+
+ validates :merge_request_id, uniqueness: { scope: :issue_id }, presence: true
+ validates :issue_id, presence: true
+end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 332e297840cd5b4ef5504745acc6bd1764e5869c..374689af0d3a3f5cc87233779ed25bf6b972ae70 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -147,6 +147,10 @@ def lfs_enabled?
Gitlab.config.lfs.enabled
end
+ def actual_size_limit
+ current_application_settings.repository_size_limit
+ end
+
private
def repository_storage_paths
diff --git a/app/models/note.rb b/app/models/note.rb
index de01211ea4bcbadc9f700c0da2c2dfb1f2141be2..f009fab371d3ac05c0a31d96e6e17ffea6cd8f35 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -229,10 +229,6 @@ def has_referenced_mentionables?(user)
end
end
- def user_authored?(user)
- user == author
- end
-
def award_emoji?
can_be_award_emoji? && contains_emoji_only?
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 34cd50ca563a73c12d066feaaef9bb51871ce96f..082e5e4eeacacc7e57e2396b82bbddaf0549c53e 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -182,6 +182,9 @@ def set_last_activity_at
presence: true,
inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } }
+ validates :repository_size_limit,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0, allow_nil: true }
+
with_options if: :mirror? do |project|
project.validates :import_url, presence: true
project.validates :mirror_user, presence: true
@@ -1531,6 +1534,50 @@ def reset_pushes_since_gc
Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) }
end
+ def repository_and_lfs_size
+ repository_size + lfs_objects.sum(:size).to_i.to_mb
+ end
+
+ def above_size_limit?
+ return false unless size_limit_enabled?
+
+ repository_and_lfs_size > actual_size_limit
+ end
+
+ def size_to_remove
+ repository_and_lfs_size - actual_size_limit
+ end
+
+ def actual_size_limit
+ return namespace.actual_size_limit if repository_size_limit.nil?
+
+ repository_size_limit
+ end
+
+ def size_limit_enabled?
+ actual_size_limit != 0
+ end
+
+ def changes_will_exceed_size_limit?(size_mb)
+ size_limit_enabled? && (size_mb > actual_size_limit || size_mb + repository_and_lfs_size > actual_size_limit)
+ end
+
+ def environments_for(ref, commit, with_tags: false)
+ environment_ids = deployments.group(:environment_id).
+ select(:environment_id)
+
+ environment_ids =
+ if with_tags
+ environment_ids.where('ref=? OR tag IS TRUE', ref)
+ else
+ environment_ids.where(ref: ref)
+ end
+
+ environments.where(id: environment_ids).select do |environment|
+ environment.includes_commit?(commit)
+ end
+ end
+
private
def pushes_since_gc_redis_key
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 9c602c582bd24b039ee4d66cbce5fb6ebd6d091f..8c9534c3565f95b80cb6a0b12ef47e40468c4949 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -22,6 +22,12 @@ class ProjectFeature < ActiveRecord::Base
belongs_to :project
+ default_value_for :builds_access_level, value: ENABLED, allows_nil: false
+ default_value_for :issues_access_level, value: ENABLED, allows_nil: false
+ default_value_for :merge_requests_access_level, value: ENABLED, allows_nil: false
+ default_value_for :snippets_access_level, value: ENABLED, allows_nil: false
+ default_value_for :wiki_access_level, value: ENABLED, allows_nil: false
+
def feature_available?(feature, user)
raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature)
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 7c65d55647ee91e0b8fbd2a280e98af72b97bbde..a7116a249320ab0538e4ae8176e8b23996b04e53 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -924,7 +924,7 @@ def remove_file(user, path, message, branch, author_email: nil, author_name: nil
def get_committer_and_author(user, email: nil, name: nil)
committer = user_to_committer(user)
- author = name && email ? Gitlab::Git::committer_hash(email: email, name: name) : committer
+ author = Gitlab::Git::committer_hash(email: email, name: name) || committer
{
author: author,
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 06f0155a864c53accfd3c8f719112467ae110eb5..33de7df7af10468328a2dc449aca41dfcb010fa6 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -5,6 +5,7 @@ class Snippet < ActiveRecord::Base
include Referable
include Sortable
include Elastic::SnippetsSearch
+ include Awardable
default_value_for :visibility_level, Snippet::PRIVATE
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 49073df3560a60e8ef2e0ce15ace3c868545b919..a3d43692f644079ccd69033cee6804a36070116f 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -49,6 +49,7 @@ def guest_access!
can! :create_issue
can! :create_note
can! :upload_file
+ can! :read_cycle_analytics
end
def reporter_access!
@@ -224,6 +225,7 @@ def anonymous_rules
can! :read_commit_status
can! :read_container_image
can! :download_code
+ can! :read_cycle_analytics
# NOTE: may be overridden by IssuePolicy
can! :read_issue
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 98da65639475438a96ca575f7a6360bd7fd7927d..38ac66312287e2842fba59c100aba486cc2977ee 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -5,7 +5,7 @@ class ContainerRegistryAuthenticationService < BaseService
AUDIENCE = 'container_registry'
def execute(authentication_abilities:)
- @authentication_abilities = authentication_abilities || []
+ @authentication_abilities = authentication_abilities
return error('not found', 404) unless registry.enabled
diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb
index e6667132e27582f1530a25c53c334613de788ac8..799ad3e1bd0f40cc6ac5d8137e1a65e9ece4fa21 100644
--- a/app/services/create_deployment_service.rb
+++ b/app/services/create_deployment_service.rb
@@ -4,7 +4,7 @@ class CreateDeploymentService < BaseService
def execute(deployable = nil)
environment = find_or_create_environment
- project.deployments.create(
+ deployment = project.deployments.create(
environment: environment,
ref: params[:ref],
tag: params[:tag],
@@ -12,6 +12,10 @@ def execute(deployable = nil)
user: current_user,
deployable: deployable
)
+
+ deployment.update_merge_request_metrics!
+
+ deployment
end
private
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index 4ccd02d15f4859a7f91e2b90162782f48a36651e..91deb36a1a98d2f660cecde719f68180a93983a4 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -49,6 +49,10 @@ def raise_error(message)
def validate
allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@target_branch)
+ if project.above_size_limit?
+ raise_error(Gitlab::RepositorySizeError.new(project).commit_error)
+ end
+
unless allowed
raise_error("You are not allowed to push into this branch")
end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index b4b78854a053cfba612440b814f9235633188cc2..56898c6c7a3fc3528ad00bb116c58d48bff7ee20 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -139,6 +139,7 @@ def process_commit_messages
end
commit.create_cross_references!(authors[commit], closed_issues)
+ update_issue_metrics(commit, authors)
end
end
@@ -191,4 +192,11 @@ def commit_user(commit)
def branch_name
@branch_name ||= Gitlab::Git.ref_name(params[:ref])
end
+
+ def update_issue_metrics(commit, authors)
+ mentioned_issues = commit.all_references(authors[commit]).issues
+
+ Issue::Metrics.where(issue_id: mentioned_issues.map(&:id), first_mentioned_in_commit_at: nil).
+ update_all(first_mentioned_in_commit_at: commit.committed_date)
+ end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 4c8d93999a7049f218ea3a273a4a7dd47b03e041..fbce46769f7648f8687aeb87d9704803f3346303 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -157,6 +157,10 @@ def after_create(issuable)
# To be overridden by subclasses
end
+ def after_update(issuable)
+ # To be overridden by subclasses
+ end
+
def update_issuable(issuable, attributes)
issuable.with_transaction_returning_status do
issuable.update(attributes.merge(updated_by: current_user))
@@ -182,6 +186,7 @@ def update(issuable)
end
handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users)
+ after_update(issuable)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
end
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 73247e62421b3576f5068d52d97bf6b5023e0032..b0ae2dfe4ce532a9d6f76bbdd54dc926e5f6098e 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -20,6 +20,7 @@ def after_create(issuable)
event_service.open_mr(issuable, current_user)
notification_service.new_merge_request(issuable, current_user)
todo_service.new_merge_request(issuable, current_user)
+ issuable.cache_merge_request_closes_issues!(current_user)
end
end
end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 33e3a8daafbb620705ec828e75b63370e3dec4fd..a599a4eb0af8a79f8ffb210ea764017f9f222df1 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -18,6 +18,12 @@ def execute(merge_request)
return error('Merge request is not mergeable') unless @merge_request.mergeable?
+ if @merge_request.target_project.above_size_limit?
+ message = Gitlab::RepositorySizeError.new(@merge_request.target_project).merge_error
+ @merge_request.update(merge_error: message)
+ return error(message)
+ end
+
merge_request.in_locked_state do
if commit
after_merge
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 9c6a60664688781e06f754f4ded2a1c02bcc6fa5..2ca93afe6fc3bde631227439a9c4068c920f0340 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -13,6 +13,7 @@ def execute(oldrev, newrev, ref)
reload_merge_requests
reset_merge_when_build_succeeds
mark_pending_todos_done
+ cache_merge_requests_closing_issues
# Leave a system note if a branch was deleted/added
if branch_added? || branch_removed?
@@ -156,6 +157,14 @@ def execute_mr_web_hooks
end
end
+ # If the merge requests closes any issues, save this information in the
+ # `MergeRequestsClosingIssues` model (as a performance optimization).
+ def cache_merge_requests_closing_issues
+ @project.merge_requests.where(source_branch: @branch_name).each do |merge_request|
+ merge_request.cache_merge_request_closes_issues!(@current_user)
+ end
+ end
+
def filter_merge_requests(merge_requests)
merge_requests.uniq.select(&:source_project)
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index f95b6c3ca50f847b0da7a3288ced14592125067e..65076df251564f94a09042562a1b4e5469da2d2e 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -87,5 +87,9 @@ def reopen_service
def close_service
MergeRequests::CloseService
end
+
+ def after_update(issuable)
+ issuable.cache_merge_request_closes_issues!(current_user)
+ end
end
end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index df060ef59c2519a9c85df8d8ee49b6afb334523b..fa23e2dee16e175de0c079990bb3305387003c04 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -49,39 +49,6 @@
= select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
%span.help-block#clone-protocol-help
Allow only the selected protocols to be used for Git access.
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :version_check_enabled do
- = f.check_box :version_check_enabled
- Version check enabled
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :usage_ping_enabled do
- = f.check_box :usage_ping_enabled
- Usage ping enabled
- .container
- .help-block
- Every week GitLab will report license usage back to GitLab, Inc.
- Disable this option if you do not want this to occur. This is the JSON payload that will be sent:
- %pre.usage-data.js-syntax-highlight.code.highlight{ "data-endpoint" => usage_data_admin_application_settings_path(format: :html) }
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :email_author_in_body do
- = f.check_box :email_author_in_body
- Include author name in notification email body
- .help-block
- Some email servers do not support overriding the email sender name.
- Enable this option to include the name of the author of the issue,
- merge request or comment in the email body instead.
- .form-group
- = f.label :admin_notification_email, class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_field :admin_notification_email, class: 'form-control'
- .help-block
- Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.
%fieldset
%legend Account and Limit Settings
@@ -99,6 +66,14 @@
= f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :max_attachment_size, class: 'form-control'
+ .form-group
+ = f.label :repository_size_limit, class: 'control-label col-sm-2' do
+ Size limit per repository (MB)
+ .col-sm-10
+ = f.number_field :repository_size_limit, class: 'form-control', min: 0
+ %span.help-block#repository_size_limit_help_block
+ Includes LFS objects. It can be overridden per group, or per project. 0 for unlimited.
+ = link_to icon('question-circle'), help_page_path("user/admin_area/settings/account_and_limit_settings")
.form-group
= f.label :session_expire_delay, 'Session duration (minutes)', class: 'control-label col-sm-2'
.col-sm-10
@@ -364,6 +339,15 @@
Generate API key at
%a{ href: 'http://www.akismet.com', target: 'blank'} http://www.akismet.com
+ %fieldset
+ %legend Abuse reports
+ .form-group
+ = f.label :admin_notification_email, 'Abuse reports notification email', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :admin_notification_email, class: 'form-control'
+ .help-block
+ Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.
+
%fieldset
%legend Error Reporting and Logging
%p
@@ -459,6 +443,41 @@
= succeed "." do
= link_to "Koding administration documentation", help_page_path("administration/integration/koding")
+ %fieldset
+ %legend Usage statistics
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :version_check_enabled do
+ = f.check_box :version_check_enabled
+ Version check enabled
+ .help-block
+ Let GitLab inform you when an update is available.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :usage_ping_enabled do
+ = f.check_box :usage_ping_enabled
+ Usage ping enabled
+ = link_to icon('question-circle'), help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-data")
+ .container
+ .help-block
+ Every week GitLab will report license usage back to GitLab, Inc.
+ Disable this option if you do not want this to occur. This is the JSON payload that will be sent:
+ %pre.usage-data.js-syntax-highlight.code.highlight{ "data-endpoint" => usage_data_admin_application_settings_path(format: :html) }
+
+ %fieldset
+ %legend Email
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :email_author_in_body do
+ = f.check_box :email_author_in_body
+ Include author name in notification email body
+ .help-block
+ Some email servers do not support overriding the email sender name.
+ Enable this option to include the name of the author of the issue,
+ merge request or comment in the email body instead.
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 857ca9f1f1c0e36f8881723ad749731c2f98ded4..697cf5d68b084da1a0240acf052521c8942f5862 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -2,6 +2,8 @@
= form_errors(@group)
= render 'shared/group_form', f: f
+ = render 'groups/repository_size_limit_setting', f: f
+
.form-group.group-description-holder
= f.label :avatar, "Group avatar", class: 'control-label'
.col-sm-10
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index 02efcecc8895687433128af2edb40279383bd374..fbe3ab912b615f1bd60c762511967a1baa943fa4 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -1,5 +1,5 @@
- grouped_emojis = awardable.grouped_awards(with_thumbs: inline)
-.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) } }
+.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } }
- awards_sort(grouped_emojis).each do |emoji, awards|
%button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } }
= emoji_icon(emoji, sprite: false)
diff --git a/app/views/discussions/_jump_to_next.html.haml b/app/views/discussions/_jump_to_next.html.haml
index da970792b4d04a5a83c1d18bd0a85d129a9c79b8..7ed09dd1a9874b323c79ae655e435a4b51878a30 100644
--- a/app/views/discussions/_jump_to_next.html.haml
+++ b/app/views/discussions/_jump_to_next.html.haml
@@ -1,4 +1,3 @@
-- diff_notes_disabled = (@merge_request_diff.latest? && !!@start_sha) if @merge_request_diff
- discussion = local_assigns.fetch(:discussion, nil)
- if current_user
%jump-to-discussion{ "inline-template" => true, ":discussion-id" => "'#{discussion.try(:id)}'" }
@@ -6,6 +5,5 @@
%button.btn.btn-default.discussion-next-btn.has-tooltip{ "@click" => "jumpToNextUnresolvedDiscussion",
title: "Jump to next unresolved discussion",
"aria-label" => "Jump to next unresolved discussion",
- data: { container: "body" },
- disabled: diff_notes_disabled }
+ data: { container: "body" }}
= custom_icon("next_discussion")
diff --git a/app/views/groups/_repository_size_limit_setting.html.haml b/app/views/groups/_repository_size_limit_setting.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..27463737489985f2fed4d224018a747d8ccc41a4
--- /dev/null
+++ b/app/views/groups/_repository_size_limit_setting.html.haml
@@ -0,0 +1,8 @@
+- if current_user.admin?
+ .form-group
+ = f.label :repository_size_limit, class: 'control-label' do
+ Repository size limit (MB)
+ .col-sm-10
+ = f.number_field :repository_size_limit, class: 'form-control', min: 0
+ %span.help-block#repository_size_limit_help_block
+ = size_limit_message_for_group(@group)
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 596016d878a9c3b0d2dbacc57d88cc2cc63a7105..9ad943220819cfa50ec7e29eb45ee9fe512b786c 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -6,6 +6,8 @@
= form_errors(@group)
= render 'shared/group_form', f: f
+ = render 'repository_size_limit_setting', f: f
+
.form-group
.col-sm-offset-2.col-sm-10
= image_tag group_icon(@group), alt: '', class: 'avatar group-avatar s160'
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 53ed4fa991d688264891957c785f000808bc1924..31db6ee0cad954d80b751fb76f39380cba358442 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -23,7 +23,7 @@
.cover-desc.description
= markdown(@group.description, pipeline: :description)
-%div{ class: container_class }
+%div.groups-header{ class: container_class }
.top-area
%ul.nav-links
%li.active
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index c58756a7a64fce1037a0400f237b4fa324ca97d8..afaf4b703bbc23c94ac9d19f6edf203d4a01f4d2 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -47,7 +47,7 @@
Repository
- if project_nav_tab? :pipelines
- = nav_link(controller: [:pipelines, :builds, :environments]) do
+ = nav_link(controller: [:pipelines, :builds, :environments, :cycle_analytics]) do
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
%span
Pipelines
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index 9fe94291db749dfa5c8c337213dfa04339678daa..277eb71ea739e04c968590b4305e3992a64d47e6 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -14,9 +14,6 @@
window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}";
window.preview_markdown_path = "#{preview_markdown_path}";
-- content_for :scripts_body do
- = render "layouts/init_auto_complete" if current_user
-
- content_for :header_content do
.js-dropdown-menu-projects
.dropdown-menu.dropdown-select.dropdown-menu-projects
diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml
index 3978fa60d663f2396c2950c962b86985dc8ffc53..cb97181b9e19ac61124f8074a8c9622315eab578 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -7,3 +7,6 @@
= text_area_tag attr, nil, class: classes, placeholder: placeholder
%a.zen-control.zen-control-leave.js-zen-leave{ href: "#" }
= icon('compress')
+
+- content_for :scripts_body do
+ = render "layouts/init_auto_complete" if current_user && (@target_project || @project)
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..5dcb2a17873594adb66a2c185ab3ceb391a26680
--- /dev/null
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -0,0 +1,57 @@
+- @no_container = true
+- page_title "Cycle Analytics"
+= render "projects/pipelines/head"
+
+#cycle-analytics{"v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project)}}
+
+ .bordered-box.landing.content-block{"v-if" => "!isHelpDismissed"}
+ = icon('times', class: 'dismiss-icon', "@click": "dismissLanding()")
+ = custom_icon('icon_cycle_analytics_splash')
+ .inner-content
+ %h4
+ Introducing Cycle Analytics
+ %p
+ Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
+
+ = link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn'
+
+ = icon("spinner spin", "v-show" => "isLoading")
+
+ .wrapper{"v-show" => "!isLoading && !hasError"}
+ .panel.panel-default
+ .panel-heading
+ Pipeline Health
+
+ .content-block
+ .container-fluid
+ .row
+ .col-xs-3.column{"v-for" => "item in summary"}
+ %h3.header {{item.value}}
+ %p.text {{item.title}}
+
+ .col-xs-3.column
+ .dropdown.inline.js-ca-dropdown
+ %button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"}
+ %span.dropdown-label Last 30 days
+ %i.fa.fa-chevron-down
+ %ul.dropdown-menu.dropdown-menu-align-right
+ %li
+ %a{'href' => "#", 'data-value' => '30'}
+ Last 30 days
+ %li
+ %a{'href' => "#", 'data-value' => '90'}
+ Last 90 days
+
+ .bordered-box
+ %ul.content-list
+ %li{"v-for" => "item in stats"}
+ .container-fluid
+ .row
+ .col-xs-10.title-col
+ %p.title
+ {{item.title}}
+ %p.text
+ {{item.description}}
+ .col-xs-2.value-col
+ %span
+ {{item.value}}
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index c8228c040e69f54544f65703d6127e43f93c8587..1489ebb04781c80353052219134dc0cf20db10e0 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -34,6 +34,14 @@
= visibility_level_label(@project.visibility_level)
.light= visibility_level_description(@project.visibility_level, @project)
+ - if current_user.admin?
+ .form-group
+ = f.label :repository_size_limit, class: 'label-light' do
+ Repository size limit (MB)
+ = f.number_field :repository_size_limit, class: 'form-control', min: 0
+ %span.help-block#repository_size_limit_help_block
+ = size_limit_message(@project)
+
.form-group
= render 'shared/allow_request_access', form: f
diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml
index 8f7b5d1543e9b100031fb6c1d5fe01b40149810f..49819519759e6bb30908b15e7cb1380501f969ca 100644
--- a/app/views/projects/merge_requests/show/_versions.html.haml
+++ b/app/views/projects/merge_requests/show/_versions.html.haml
@@ -10,23 +10,25 @@
- else
version #{version_index(@merge_request_diff)}
%span.caret
- %ul.dropdown-menu.dropdown-menu-selectable
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Version:
- %button.dropdown-title-button.dropdown-menu-close
- %i.fa.fa-times.dropdown-menu-close-icon
- - @merge_request_diffs.each do |merge_request_diff|
- %li
- = link_to merge_request_version_path(@project, @merge_request, merge_request_diff), class: ('is-active' if merge_request_diff == @merge_request_diff) do
- %strong
- - if merge_request_diff.latest?
- latest version
- - else
- version #{version_index(merge_request_diff)}
- .monospace #{short_sha(merge_request_diff.head_commit_sha)}
- %small
- #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)},
- = time_ago_with_tooltip(merge_request_diff.created_at)
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times', class: 'dropdown-menu-close-icon')
+ .dropdown-content
+ %ul
+ - @merge_request_diffs.each do |merge_request_diff|
+ %li
+ = link_to merge_request_version_path(@project, @merge_request, merge_request_diff), class: ('is-active' if merge_request_diff == @merge_request_diff) do
+ %strong
+ - if merge_request_diff.latest?
+ latest version
+ - else
+ version #{version_index(merge_request_diff)}
+ .monospace #{short_sha(merge_request_diff.head_commit_sha)}
+ %small
+ #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)},
+ = time_ago_with_tooltip(merge_request_diff.created_at)
- if @merge_request_diff.base_commit_sha
and
@@ -38,27 +40,29 @@
- else
#{@merge_request.target_branch}
%span.caret
- %ul.dropdown-menu.dropdown-menu-selectable
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Compared with:
- %button.dropdown-title-button.dropdown-menu-close
- %i.fa.fa-times.dropdown-menu-close-icon
- - @comparable_diffs.each do |merge_request_diff|
- %li
- = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff, merge_request_diff.head_commit_sha), class: ('is-active' if merge_request_diff == @start_version) do
- %strong
- - if merge_request_diff.latest?
- latest version
- - else
- version #{version_index(merge_request_diff)}
- .monospace #{short_sha(merge_request_diff.head_commit_sha)}
- %small
- = time_ago_with_tooltip(merge_request_diff.created_at)
- %li
- = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do
- %strong
- #{@merge_request.target_branch} (base)
- .monospace #{short_sha(@merge_request_diff.base_commit_sha)}
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}}
+ = icon('times', class: 'dropdown-menu-close-icon')
+ .dropdown-content
+ %ul
+ - @comparable_diffs.each do |merge_request_diff|
+ %li
+ = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff, merge_request_diff.head_commit_sha), class: ('is-active' if merge_request_diff == @start_version) do
+ %strong
+ - if merge_request_diff.latest?
+ latest version
+ - else
+ version #{version_index(merge_request_diff)}
+ .monospace #{short_sha(merge_request_diff.head_commit_sha)}
+ %small
+ = time_ago_with_tooltip(merge_request_diff.created_at)
+ %li
+ = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do
+ %strong
+ #{@merge_request.target_branch} (base)
+ .monospace #{short_sha(@merge_request_diff.base_commit_sha)}
- unless @merge_request_diff.latest? && !@start_sha
.comments-disabled-notif.content-block
@@ -67,4 +71,4 @@
Comments are disabled because you're comparing two versions of this merge request.
- else
Comments are disabled because you're viewing an old version of this merge request.
- = link_to 'Show latest version', merge_request_version_path(@project, @merge_request, @merge_request_diff), class: 'btn btn-sm'
+ = link_to 'Show latest version', diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-sm'
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 494695a03a50e3363efe1f8bf06b2ef01aab6493..44e645a7e8113c63c71c789c39b069bca8dbaf61 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -43,15 +43,16 @@
= icon("times-circle")
Could not connect to the CI server. Please check your settings and try again.
-- @merge_request.environments.each do |environment|
- .mr-widget-heading
- .ci_widget.ci-success
- = ci_icon_for_status("success")
- %span.hidden-sm
- Deployed to
- = succeed '.' do
- = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment), class: 'environment'
- - external_url = environment.external_url
- - if external_url
- = link_to external_url, target: '_blank' do
- = icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true)
+- @merge_request.environments.sort_by(&:name).each do |environment|
+ - if can?(current_user, :read_environment, environment)
+ .mr-widget-heading
+ .ci_widget.ci-success
+ = ci_icon_for_status("success")
+ %span.hidden-sm
+ Deployed to
+ = succeed '.' do
+ = link_to environment.name, environment_path(environment), class: 'environment'
+ - external_url = environment.external_url
+ - if external_url
+ = link_to external_url, target: '_blank' do
+ = icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true)
diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml
index 110da69e35967473f9ef6a03fb7782d1325845e0..df7b82658369b978340cd2b2aa84d2b84d6a61a0 100644
--- a/app/views/projects/merge_requests/widget/_open.html.haml
+++ b/app/views/projects/merge_requests/widget/_open.html.haml
@@ -11,6 +11,8 @@
= render 'projects/merge_requests/widget/open/geo'
- if @project.archived?
= render 'projects/merge_requests/widget/open/archived'
+ - elsif @project.above_size_limit?
+ = render 'projects/merge_requests/widget/open/size_limit_reached'
- elsif @merge_request.commits.blank?
= render 'projects/merge_requests/widget/open/nothing'
- elsif @merge_request.branch_missing?
diff --git a/app/views/projects/merge_requests/widget/open/_size_limit_reached.html.haml b/app/views/projects/merge_requests/widget/open/_size_limit_reached.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..25c637f101dc158f68410fe4950672e4264872a9
--- /dev/null
+++ b/app/views/projects/merge_requests/widget/open/_size_limit_reached.html.haml
@@ -0,0 +1,8 @@
+- error_messages = Gitlab::RepositorySizeError.new(@project)
+
+%h4.size-limit-reached
+ = icon("exclamation-triangle")
+ = error_messages.merge_error
+
+%p
+ = error_messages.more_info_message
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index f611ddc8f5f514599a2b64b216a9c1b9c703734d..5f571499e8025a94623b587d9f84007bb4277e73 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -19,3 +19,9 @@
= link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
%span
Environments
+
+ - if can?(current_user, :read_cycle_analytics, @project)
+ = nav_link(controller: %w(cycle_analytics)) do
+ = link_to project_cycle_analytics_path(@project), title: 'Cycle Analytics' do
+ %span
+ Cycle Analytics
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
index a5a5619fa128c8b191bd7bd180ebbf032baaae27..4aa4ab46a2f16a7a3931ffb56c4ca00b2e5df76d 100644
--- a/app/views/projects/snippets/_actions.html.haml
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -3,7 +3,7 @@
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New Snippet" do
New Snippet
- if can?(current_user, :update_project_snippet, @snippet)
- = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-warning", title: 'Delete Snippet' do
+ = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do
Delete
- if can?(current_user, :update_project_snippet, @snippet)
= link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index b70fda88a799d0067f278770bd4970c665d48401..9503dbded13ae43766c19c3f64b52e5811832aef 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -2,13 +2,16 @@
= render 'shared/snippets/header'
-%article.file-holder.snippet-file-content
- .file-title
- = blob_icon 0, @snippet.file_name
- = @snippet.file_name
- .file-actions
- = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
- = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank"
- = render 'shared/snippets/blob'
-
-%div#notes= render "projects/notes/notes_with_form"
+.project-snippets
+ %article.file-holder.snippet-file-content
+ .file-title
+ = blob_icon 0, @snippet.file_name
+ = @snippet.file_name
+ .file-actions
+ = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
+ = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank"
+ = render 'shared/snippets/blob'
+
+ = render 'award_emoji/awards_block', awardable: @snippet, inline: true
+
+ %div#notes= render "projects/notes/notes_with_form"
diff --git a/app/views/shared/icons/_icon_cycle_analytics_splash.svg b/app/views/shared/icons/_icon_cycle_analytics_splash.svg
new file mode 100644
index 0000000000000000000000000000000000000000..eb5a962d651a452b7d5da4db2b83bcb923e3cb68
--- /dev/null
+++ b/app/views/shared/icons/_icon_cycle_analytics_splash.svg
@@ -0,0 +1 @@
+
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index fb592c2b1e2448982457a8a5f51cae6689bb76ae..1d9b09a5ef1429176bc912d53b24b7579337c644 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -1,27 +1,25 @@
%ul.nav-links.issues-state-filters
- if defined?(type) && type == :merge_requests
- page_context_word = 'merge requests'
- - records = @all_merge_requests
- else
- page_context_word = 'issues'
- - records = @all_issues
%li{class: ("active" if params[:state] == 'opened')}
= link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do
- #{state_filters_text_for(:opened, records)}
+ #{state_filters_text_for(:opened, @project)}
- if defined?(type) && type == :merge_requests
%li{class: ("active" if params[:state] == 'merged')}
= link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do
- #{state_filters_text_for(:merged, records)}
+ #{state_filters_text_for(:merged, @project)}
%li{class: ("active" if params[:state] == 'closed')}
= link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do
- #{state_filters_text_for(:closed, records)}
+ #{state_filters_text_for(:closed, @project)}
- else
%li{class: ("active" if params[:state] == 'closed')}
= link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do
- #{state_filters_text_for(:closed, records)}
+ #{state_filters_text_for(:closed, @project)}
%li{class: ("active" if params[:state] == 'all')}
= link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do
- #{state_filters_text_for(:all, records)}
+ #{state_filters_text_for(:all, @project)}
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index fa403da8f796253ad5a6afa3a46d46e170803519..cd89155c616b6622958b5d8cd9c43ead8b848c19 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -10,3 +10,5 @@
= clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
= link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
= render 'shared/snippets/blob'
+
+= render 'award_emoji/awards_block', awardable: @snippet, inline: true
\ No newline at end of file
diff --git a/config/application.rb b/config/application.rb
index c418ab1b7ee186a3275ea46589e1d4d1a1050125..9abec809c8203ae0a75c5d40744e402afc39a0c6 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -119,10 +119,6 @@ class Application < Rails::Application
redis_config_hash = Gitlab::Redis.params
redis_config_hash[:namespace] = Gitlab::Redis::CACHE_NAMESPACE
redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever
- if Sidekiq.server? # threaded context
- redis_config_hash[:pool_size] = Sidekiq.options[:concurrency] + 5
- redis_config_hash[:pool_timeout] = 1
- end
config.cache_store = :redis_store, redis_config_hash
config.active_record.raise_in_transactional_callbacks = true
diff --git a/config/routes.rb b/config/routes.rb
index 422e9d0230914977181039f29b41066b7e2b39ee..f6ccfa7db528467dcad9e7a44e6ed343e1c4f3d8 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -35,6 +35,10 @@
post :approve_access_request, on: :member
end
+ concern :awardable do
+ post :toggle_award_emoji, on: :member
+ end
+
namespace :ci do
# CI API
Ci::API::API.logger Rails.logger
@@ -106,7 +110,7 @@
#
# Global snippets
#
- resources :snippets do
+ resources :snippets, concerns: :awardable do
member do
get 'raw'
end
@@ -118,7 +122,6 @@
#
# Invites
#
-
resources :invites, only: [:show], constraints: { id: /[A-Za-z0-9_-]+/ } do
member do
post :accept
@@ -711,7 +714,7 @@
end
end
- resources :snippets, constraints: { id: /\d+/ } do
+ resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
member do
get 'raw'
end
@@ -773,7 +776,7 @@
end
end
- resources :merge_requests, constraints: { id: /\d+/ } do
+ resources :merge_requests, concerns: :awardable, constraints: { id: /\d+/ } do
member do
get :commits
get :diffs
@@ -787,7 +790,6 @@
post :toggle_subscription
post :approve
post :rebase
- post :toggle_award_emoji
post :remove_wip
get :diff_for_path
post :resolve_conflicts
@@ -850,6 +852,8 @@
resources :environments
+ resource :cycle_analytics, only: [:show]
+
resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
collection do
post :cancel_all
@@ -908,10 +912,9 @@
end
end
- resources :issues, constraints: { id: /\d+/ } do
+ resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do
member do
post :toggle_subscription
- post :toggle_award_emoji
post :mark_as_spam
get :referenced_merge_requests
get :related_branches
@@ -939,9 +942,8 @@
resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ }
- resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do
+ resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
member do
- post :toggle_award_emoji
delete :delete_attachment
post :resolve
delete :resolve, action: :unresolve
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e882a492757053130531f959fcf4b6669bff5e36
--- /dev/null
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -0,0 +1,246 @@
+require 'sidekiq/testing'
+require './spec/support/test_env'
+
+class Gitlab::Seeder::CycleAnalytics
+ def initialize(project, perf: false)
+ @project = project
+ @user = User.order(:id).last
+ @issue_count = perf ? 1000 : 5
+ stub_git_pre_receive!
+ end
+
+ # The GitLab API needn't be running for the fixtures to be
+ # created. Since we're performing a number of git actions
+ # here (like creating a branch or committing a file), we need
+ # to disable the `pre_receive` hook in order to remove this
+ # dependency on the GitLab API.
+ def stub_git_pre_receive!
+ GitHooksService.class_eval do
+ def run_hook(name)
+ [true, '']
+ end
+ end
+ end
+
+ def seed_metrics!
+ @issue_count.times do |index|
+ # Issue
+ Timecop.travel 5.days.from_now
+ title = "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}"
+ issue = Issue.create(project: @project, title: title, author: @user)
+ issue_metrics = issue.metrics
+
+ # Milestones / Labels
+ Timecop.travel 5.days.from_now
+ if index.even?
+ issue_metrics.first_associated_with_milestone_at = rand(6..12).hours.from_now
+ else
+ issue_metrics.first_added_to_board_at = rand(6..12).hours.from_now
+ end
+
+ # Commit
+ Timecop.travel 5.days.from_now
+ issue_metrics.first_mentioned_in_commit_at = rand(6..12).hours.from_now
+
+ # MR
+ Timecop.travel 5.days.from_now
+ branch_name = "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}"
+ @project.repository.add_branch(@user, branch_name, 'master')
+ merge_request = MergeRequest.create(target_project: @project, source_project: @project, source_branch: branch_name, target_branch: 'master', title: branch_name, author: @user)
+ merge_request_metrics = merge_request.metrics
+
+ # MR closing issues
+ Timecop.travel 5.days.from_now
+ MergeRequestsClosingIssues.create!(issue: issue, merge_request: merge_request)
+
+ # Merge
+ Timecop.travel 5.days.from_now
+ merge_request_metrics.merged_at = rand(6..12).hours.from_now
+
+ # Start build
+ Timecop.travel 5.days.from_now
+ merge_request_metrics.latest_build_started_at = rand(6..12).hours.from_now
+
+ # Finish build
+ Timecop.travel 5.days.from_now
+ merge_request_metrics.latest_build_finished_at = rand(6..12).hours.from_now
+
+ # Deploy to production
+ Timecop.travel 5.days.from_now
+ merge_request_metrics.first_deployed_to_production_at = rand(6..12).hours.from_now
+
+ issue_metrics.save!
+ merge_request_metrics.save!
+
+ print '.'
+ end
+ end
+
+ def seed!
+ Sidekiq::Testing.inline! do
+ issues = create_issues
+ puts '.'
+
+ # Stage 1
+ Timecop.travel 5.days.from_now
+ add_milestones_and_list_labels(issues)
+ print '.'
+
+ # Stage 2
+ Timecop.travel 5.days.from_now
+ branches = mention_in_commits(issues)
+ print '.'
+
+ # Stage 3
+ Timecop.travel 5.days.from_now
+ merge_requests = create_merge_requests_closing_issues(issues, branches)
+ print '.'
+
+ # Stage 4
+ Timecop.travel 5.days.from_now
+ run_builds(merge_requests)
+ print '.'
+
+ # Stage 5
+ Timecop.travel 5.days.from_now
+ merge_merge_requests(merge_requests)
+ print '.'
+
+ # Stage 6 / 7
+ Timecop.travel 5.days.from_now
+ deploy_to_production(merge_requests)
+ print '.'
+ end
+
+ print '.'
+ end
+
+ private
+
+ def create_issues
+ Array.new(@issue_count) do
+ issue_params = {
+ title: "Cycle Analytics: #{FFaker::Lorem.sentence(6)}",
+ description: FFaker::Lorem.sentence,
+ state: 'opened',
+ assignee: @project.team.users.sample
+ }
+
+ Issues::CreateService.new(@project, @project.team.users.sample, issue_params).execute
+ end
+ end
+
+ def add_milestones_and_list_labels(issues)
+ issues.shuffle.map.with_index do |issue, index|
+ Timecop.travel 12.hours.from_now
+
+ if index.even?
+ issue.update(milestone: @project.milestones.sample)
+ else
+ label_name = "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}"
+ list_label = FactoryGirl.create(:label, title: label_name, project: issue.project)
+ FactoryGirl.create(:list, board: FactoryGirl.create(:board, project: issue.project), label: list_label)
+ issue.update(labels: [list_label])
+ end
+
+ issue
+ end
+ end
+
+ def mention_in_commits(issues)
+ issues.map do |issue|
+ Timecop.travel 12.hours.from_now
+
+ branch_name = filename = "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}"
+
+ issue.project.repository.add_branch(@user, branch_name, 'master')
+
+ options = {
+ committer: issue.project.repository.user_to_committer(@user),
+ author: issue.project.repository.user_to_committer(@user),
+ commit: { message: "Commit for ##{issue.iid}", branch: branch_name, update_ref: true },
+ file: { content: "content", path: filename, update: false }
+ }
+
+ commit_sha = Gitlab::Git::Blob.commit(issue.project.repository, options)
+ issue.project.repository.commit(commit_sha)
+
+
+ GitPushService.new(issue.project,
+ @user,
+ oldrev: issue.project.repository.commit("master").sha,
+ newrev: commit_sha,
+ ref: 'refs/heads/master').execute
+
+ branch_name
+ end
+ end
+
+ def create_merge_requests_closing_issues(issues, branches)
+ issues.zip(branches).map do |issue, branch|
+ Timecop.travel 12.hours.from_now
+
+ opts = {
+ title: 'Cycle Analytics merge_request',
+ description: "Fixes #{issue.to_reference}",
+ source_branch: branch,
+ target_branch: 'master'
+ }
+
+ MergeRequests::CreateService.new(issue.project, @user, opts).execute
+ end
+ end
+
+ def run_builds(merge_requests)
+ merge_requests.each do |merge_request|
+ Timecop.travel 12.hours.from_now
+
+ service = Ci::CreatePipelineService.new(merge_request.project,
+ @user,
+ ref: "refs/heads/#{merge_request.source_branch}")
+ pipeline = service.execute(ignore_skip_ci: true, save_on_errors: false)
+
+ pipeline.run!
+ Timecop.travel rand(1..6).hours.from_now
+ pipeline.succeed!
+ end
+ end
+
+ def merge_merge_requests(merge_requests)
+ merge_requests.each do |merge_request|
+ Timecop.travel 12.hours.from_now
+
+ MergeRequests::MergeService.new(merge_request.project, @user).execute(merge_request)
+ end
+ end
+
+ def deploy_to_production(merge_requests)
+ merge_requests.each do |merge_request|
+ Timecop.travel 12.hours.from_now
+
+ CreateDeploymentService.new(merge_request.project, @user, {
+ environment: 'production',
+ ref: 'master',
+ tag: false,
+ sha: @project.repository.commit('master').sha
+ }).execute
+ end
+ end
+end
+
+Gitlab::Seeder.quiet do
+ if ENV['SEED_CYCLE_ANALYTICS']
+ Project.all.each do |project|
+ seeder = Gitlab::Seeder::CycleAnalytics.new(project)
+ seeder.seed!
+ end
+ elsif ENV['CYCLE_ANALYTICS_PERF_TEST']
+ seeder = Gitlab::Seeder::CycleAnalytics.new(Project.order(:id).first, perf: true)
+ seeder.seed!
+ elsif ENV['CYCLE_ANALYTICS_POPULATE_METRICS_DIRECTLY']
+ seeder = Gitlab::Seeder::CycleAnalytics.new(Project.order(:id).first, perf: true)
+ seeder.seed_metrics!
+ else
+ puts "Not running the cycle analytics seed file. Use the `SEED_CYCLE_ANALYTICS` environment variable to enable it."
+ end
+end
diff --git a/db/migrate/20160824124900_add_table_issue_metrics.rb b/db/migrate/20160824124900_add_table_issue_metrics.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e9bb79b3c628f10f3b86ac9ea58d8451f00f8424
--- /dev/null
+++ b/db/migrate/20160824124900_add_table_issue_metrics.rb
@@ -0,0 +1,37 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddTableIssueMetrics < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = true
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ DOWNTIME_REASON = 'Adding foreign key'
+
+ # 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
+ create_table :issue_metrics do |t|
+ t.references :issue, index: { name: "index_issue_metrics" }, foreign_key: { on_delete: :cascade }, null: false
+
+ t.datetime 'first_mentioned_in_commit_at'
+ t.datetime 'first_associated_with_milestone_at'
+ t.datetime 'first_added_to_board_at'
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20160825052008_add_table_merge_request_metrics.rb b/db/migrate/20160825052008_add_table_merge_request_metrics.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e01cc5038b900efa9cfee7b6421750e1eeb11859
--- /dev/null
+++ b/db/migrate/20160825052008_add_table_merge_request_metrics.rb
@@ -0,0 +1,38 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddTableMergeRequestMetrics < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = true
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ DOWNTIME_REASON = 'Adding foreign key'
+
+ # 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
+ create_table :merge_request_metrics do |t|
+ t.references :merge_request, index: { name: "index_merge_request_metrics" }, foreign_key: { on_delete: :cascade }, null: false
+
+ t.datetime 'latest_build_started_at'
+ t.datetime 'latest_build_finished_at'
+ t.datetime 'first_deployed_to_production_at', index: true
+ t.datetime 'merged_at'
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20160829104026_add_repository_size_limit_to_application_settings.rb b/db/migrate/20160829104026_add_repository_size_limit_to_application_settings.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f934281cc9bf29884c980c7226b1ecd6e827559c
--- /dev/null
+++ b/db/migrate/20160829104026_add_repository_size_limit_to_application_settings.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddRepositorySizeLimitToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :repository_size_limit, :integer, default: 0
+ end
+end
diff --git a/db/migrate/20160913172608_add_repository_size_limit_to_projects.rb b/db/migrate/20160913172608_add_repository_size_limit_to_projects.rb
new file mode 100644
index 0000000000000000000000000000000000000000..636f3b661182b990fda2b4844c52d8d9c512cc2e
--- /dev/null
+++ b/db/migrate/20160913172608_add_repository_size_limit_to_projects.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddRepositorySizeLimitToProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :projects, :repository_size_limit, :integer
+ end
+end
diff --git a/db/migrate/20160913172737_add_repository_size_limit_to_namespaces.rb b/db/migrate/20160913172737_add_repository_size_limit_to_namespaces.rb
new file mode 100644
index 0000000000000000000000000000000000000000..23955d8ab121a026b441edcba6c96e4307b4050f
--- /dev/null
+++ b/db/migrate/20160913172737_add_repository_size_limit_to_namespaces.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddRepositorySizeLimitToNamespaces < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :namespaces, :repository_size_limit, :integer
+ end
+end
diff --git a/db/migrate/20160915042921_create_merge_requests_closing_issues.rb b/db/migrate/20160915042921_create_merge_requests_closing_issues.rb
new file mode 100644
index 0000000000000000000000000000000000000000..94874a853dae6e15df78dd78a7ebfbb10a3be4ed
--- /dev/null
+++ b/db/migrate/20160915042921_create_merge_requests_closing_issues.rb
@@ -0,0 +1,34 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class CreateMergeRequestsClosingIssues < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = true
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ DOWNTIME_REASON = 'Adding foreign keys'
+
+ # 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
+ create_table :merge_requests_closing_issues do |t|
+ t.references :merge_request, foreign_key: { on_delete: :cascade }, index: true, null: false
+ t.references :issue, foreign_key: { on_delete: :cascade }, index: true, null: false
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index b29e1c36c56151542b483381246282630e27d330..f0281d9ac99f33a9be47332f27a7d8e507c1f31b 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160913212128) do
+ActiveRecord::Schema.define(version: 20160915042921) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -100,6 +100,7 @@
t.boolean "usage_ping_enabled", default: true, null: false
t.boolean "koding_enabled"
t.string "koding_url"
+ t.integer "repository_size_limit", default: 0
end
create_table "approvals", force: :cascade do |t|
@@ -184,9 +185,9 @@
t.text "commands"
t.integer "job_id"
t.string "name"
- t.boolean "deploy", default: false
+ t.boolean "deploy", default: false
t.text "options"
- t.boolean "allow_failure", default: false, null: false
+ t.boolean "allow_failure", default: false, null: false
t.string "stage"
t.integer "trigger_request_id"
t.integer "stage_idx"
@@ -498,6 +499,17 @@
add_index "index_statuses", ["project_id"], name: "index_index_statuses_on_project_id", unique: true, using: :btree
+ create_table "issue_metrics", force: :cascade do |t|
+ t.integer "issue_id", null: false
+ t.datetime "first_mentioned_in_commit_at"
+ t.datetime "first_associated_with_milestone_at"
+ t.datetime "first_added_to_board_at"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "issue_metrics", ["issue_id"], name: "index_issue_metrics", using: :btree
+
create_table "issues", force: :cascade do |t|
t.string "title"
t.integer "assignee_id"
@@ -658,6 +670,19 @@
add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", using: :btree
+ create_table "merge_request_metrics", force: :cascade do |t|
+ t.integer "merge_request_id", null: false
+ t.datetime "latest_build_started_at"
+ t.datetime "latest_build_finished_at"
+ t.datetime "first_deployed_to_production_at"
+ t.datetime "merged_at"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "merge_request_metrics", ["first_deployed_to_production_at"], name: "index_merge_request_metrics_on_first_deployed_to_production_at", using: :btree
+ add_index "merge_request_metrics", ["merge_request_id"], name: "index_merge_request_metrics", using: :btree
+
create_table "merge_requests", force: :cascade do |t|
t.string "target_branch", null: false
t.string "source_branch", null: false
@@ -701,6 +726,16 @@
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
+ create_table "merge_requests_closing_issues", force: :cascade do |t|
+ t.integer "merge_request_id", null: false
+ t.integer "issue_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "merge_requests_closing_issues", ["issue_id"], name: "index_merge_requests_closing_issues_on_issue_id", using: :btree
+ add_index "merge_requests_closing_issues", ["merge_request_id"], name: "index_merge_requests_closing_issues_on_merge_request_id", using: :btree
+
create_table "milestones", force: :cascade do |t|
t.string "title", null: false
t.integer "project_id", null: false
@@ -739,6 +774,7 @@
t.datetime "ldap_sync_last_successful_update_at"
t.datetime "ldap_sync_last_sync_at"
t.boolean "lfs_enabled"
+ t.integer "repository_size_limit"
end
add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
@@ -959,6 +995,7 @@
t.boolean "has_external_wiki"
t.boolean "repository_read_only"
t.boolean "lfs_enabled"
+ t.integer "repository_size_limit"
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
@@ -1310,8 +1347,12 @@
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
add_foreign_key "boards", "projects"
+ add_foreign_key "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "lists", "boards"
add_foreign_key "lists", "labels"
+ add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
+ add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
+ add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
add_foreign_key "path_locks", "projects"
add_foreign_key "path_locks", "users"
add_foreign_key "personal_access_tokens", "users"
diff --git a/doc/README.md b/doc/README.md
index 7499a486ddeb8995701a5754bacc3de5b49069d2..7a1bc5140c65211a63244cd2aea7652488cd9113 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -39,9 +39,10 @@
- [Install](install/README.md) Requirements, directory structures and installation from source.
- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, LDAP and Twitter.
- [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components.
-- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages.
+- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter.
+- [Issue closing pattern](administration/issue_closing_pattern.md) Customize how to close an issue from commit messages.
- [Koding](administration/integration/koding.md) Set up Koding to use with GitLab.
-- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars.
+- [Libravatar](customization/libravatar.md) Use Libravatar instead of Gravatar for user avatars.
- [Log system](administration/logs.md) Log system.
- [Environment Variables](administration/environment_variables.md) to configure GitLab.
- [Operations](operations/README.md) Keeping GitLab up and running.
@@ -68,6 +69,7 @@
- [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability.
- [Container Registry](administration/container_registry.md) Configure Docker Registry with GitLab.
- [Multiple mountpoints for the repositories storage](administration/repository_storages.md) Define multiple repository storage paths to distribute the storage load.
+- [Repository restrictions](administration/repository_restrictions.md) Define size restrictions for your repositories to limit the space they occupy in your storage device. Includes LFS objects.
## Contributor documentation
diff --git a/doc/administration/issue_closing_pattern.md b/doc/administration/issue_closing_pattern.md
new file mode 100644
index 0000000000000000000000000000000000000000..28e1fd4e12e2d09b628a4a560a7634f392cdd125
--- /dev/null
+++ b/doc/administration/issue_closing_pattern.md
@@ -0,0 +1,49 @@
+# Issue closing pattern
+
+>**Note:**
+This is the administration documentation.
+There is a separate [user documentation] on issue closing pattern.
+
+When a commit or merge request resolves one or more issues, it is possible to
+automatically have these issues closed when the commit or merge request lands
+in the project's default branch.
+
+## Change the issue closing pattern
+
+In order to change the pattern you need to have access to the server that GitLab
+is installed on.
+
+The default pattern can be located in [gitlab.yml.example] under the
+"Automatic issue closing" section.
+
+> **Tip:**
+You are advised to use http://rubular.com to test the issue closing pattern.
+Because Rubular doesn't understand `%{issue_ref}`, you can replace this by
+`#\d+` when testing your patterns, which matches only local issue references like `#123`.
+
+**For Omnibus installations**
+
+1. Open `/etc/gitlab/gitlab.rb` with your editor.
+1. Change the value of `gitlab_rails['issue_closing_pattern']` to a regular
+ expression of your liking:
+
+ ```ruby
+ gitlab_rails['issue_closing_pattern'] = "((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)"
+ ```
+1. [Reconfigure] GitLab for the changes to take effect.
+
+**For installations from source**
+
+1. Open `gitlab.yml` with your editor.
+1. Change the value of `issue_closing_pattern`:
+
+ ```yaml
+ issue_closing_pattern: "((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)"
+ ```
+
+1. [Restart] GitLab for the changes to take effect.
+
+[gitlab.yml.example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example
+[reconfigure]: restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart]: restart_gitlab.md#installations-from-source
+[user documentation]: ../user/project/issues/automatic_issue_closing.md
diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md
index 72ec99b7c56f3f411cd58cace0130ba6c6c09a8f..c464e3f3f71261c622ca1d7908e29850dffc3406 100644
--- a/doc/api/award_emoji.md
+++ b/doc/api/award_emoji.md
@@ -1,12 +1,13 @@
# Award Emoji
-> [Introduced][ce-4575] in GitLab 8.9.
+> [Introduced][ce-4575] in GitLab 8.9, Snippet support in 8.12
+
An awarded emoji tells a thousand words, and can be awarded on issues, merge
-requests and notes/comments. Issues, merge requests and notes are further called
+requests, snippets, and notes/comments. Issues, merge requests, snippets, and notes are further called
`awardables`.
-## Issues and merge requests
+## Issues, merge requests, and snippets
### List an awardable's award emoji
@@ -15,6 +16,7 @@ Gets a list of all award emoji
```
GET /projects/:id/issues/:issue_id/award_emoji
GET /projects/:id/merge_requests/:merge_request_id/award_emoji
+GET /projects/:id/snippets/:snippet_id/award_emoji
```
Parameters:
@@ -69,11 +71,12 @@ Example Response:
### Get single award emoji
-Gets a single award emoji from an issue or merge request.
+Gets a single award emoji from an issue, snippet, or merge request.
```
GET /projects/:id/issues/:issue_id/award_emoji/:award_id
GET /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id
+GET /projects/:id/snippets/:snippet_id/award_emoji/:award_id
```
Parameters:
@@ -116,6 +119,7 @@ This end point creates an award emoji on the specified resource
```
POST /projects/:id/issues/:issue_id/award_emoji
POST /projects/:id/merge_requests/:merge_request_id/award_emoji
+POST /projects/:id/snippets/:snippet_id/award_emoji
```
Parameters:
@@ -159,6 +163,7 @@ admins or the author of the award. Status code 200 on success, 401 if unauthoriz
```
DELETE /projects/:id/issues/:issue_id/award_emoji/:award_id
DELETE /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id
+DELETE /projects/:id/snippets/:snippet_id/award_emoji/:award_id
```
Parameters:
@@ -197,7 +202,7 @@ Example Response:
## Award Emoji on Notes
The endpoints documented above are available for Notes as well. Notes
-are a sub-resource of Issues and Merge Requests. The examples below
+are a sub-resource of Issues, Merge Requests, or Snippets. The examples below
describe working with Award Emoji on notes for an Issue, but can be
easily adapted for notes on a Merge Request.
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 10ce4ac8940024e6f0da941de49e5e7118152ac3..341bc85a16abf038a3ce49ee22fcd06266e8f688 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -16,5 +16,7 @@
- [Trigger builds through the API](triggers/README.md)
- [Build artifacts](../user/project/builds/artifacts.md)
- [User permissions](../user/permissions.md#gitlab-ci)
+- [Build permissions](../user/permissions.md#build-permissions)
- [API](../api/ci/README.md)
- [CI services (linked docker containers)](services/README.md)
+- [**New CI build permissions model**](../user/project/new_ci_build_permissions_model.md) Read about what changed in GitLab 8.12 and how that affects your builds. There's a new way to access your Git submodules and LFS objects in builds.
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 6c6767fea0b14ec04031e1edb0414226aa58ca9c..b78422f6d0e9aa650e51d6754e7f4a937ed9ad04 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -2,6 +2,10 @@
> [Introduced][ci-229] in GitLab CE 7.14.
+> **Note**:
+GitLab 8.12 has a completely redesigned build permissions system.
+Read all about the [new model and its implications][../../user/project/new_ci_build_permissions_model.md#build-triggers].
+
Triggers can be used to force a rebuild of a specific branch, tag or commit,
with an API call.
diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md
index 4620bb2dcde68372f9ce052c381faa94b63c3a9a..31164ccd465633457d454d2f6838fb4c9c3af0ef 100644
--- a/doc/customization/issue_closing.md
+++ b/doc/customization/issue_closing.md
@@ -1,39 +1,4 @@
-# Issue closing pattern
+This document was split into:
-When a commit or merge request resolves one or more issues, it is possible to automatically have these issues closed when the commit or merge request lands in the project's default branch.
-
-If a commit message or merge request description contains a sentence matching the regular expression below, all issues referenced from
-the matched text will be closed. This happens when the commit is pushed to a project's default branch, or when a commit or merge request is merged into there.
-
-When not specified, the default `issue_closing_pattern` as shown below will be used:
-
-```bash
-((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)
-```
-
-Here, `%{issue_ref}` is a complex regular expression defined inside GitLab, that matches a reference to a local issue (`#123`), cross-project issue (`group/project#123`) or a link to an issue (`https://gitlab.example.com/group/project/issues/123`).
-
-For example:
-
-```
-git commit -m "Awesome commit message (Fix #20, Fixes #21 and Closes group/otherproject#22). This commit is also related to #17 and fixes #18, #19 and https://gitlab.example.com/group/otherproject/issues/23."
-```
-
-will close `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed to, as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as it does not match the pattern. It also works with multiline commit messages.
-
-Tip: you can test this closing pattern at [http://rubular.com][1]. Use this site
-to test your own patterns.
-Because Rubular doesn't understand `%{issue_ref}`, you can replace this by `#\d+` in testing, which matches only local issue references like `#123`.
-
-## Change the pattern
-
-For Omnibus installs you can change the default pattern in `/etc/gitlab/gitlab.rb`:
-
-```
-issue_closing_pattern: '((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)'
-```
-
-For manual installs you can customize the pattern in [gitlab.yml][0] using the `issue_closing_pattern` key.
-
-[0]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example
-[1]: http://rubular.com/r/Xmbexed1OJ
+- [administration/issue_closing_pattern.md](../administration/issue_closing_pattern.md).
+- [user/project/issues/automatic_issue_closing](../user/project/issues/automatic_issue_closing.md).
diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md
index 5221d85b6610274dd129285b5ae29c0ce57a2e9a..da9a165b8f5f7710b49bcc8872d0f051da1192d6 100644
--- a/doc/gitlab-basics/create-issue.md
+++ b/doc/gitlab-basics/create-issue.md
@@ -1,6 +1,6 @@
# How to create an Issue in GitLab
-The Issue Tracker is a good place to add things that need to be improved or solved in a project.
+The Issue Tracker is a good place to add things that need to be improved or solved in a project.
To create an Issue, sign in to GitLab.
@@ -24,4 +24,4 @@ You may assign the Issue to a user, add a milestone and add labels (they are all

-Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](http://docs.gitlab.com/ce/customization/issue_closing.html).
+Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](../user/project/issues/automatic_issue_closing.md).
diff --git a/doc/intro/README.md b/doc/intro/README.md
index 71fef50ceb45a30cf702866fe0c00dc8101221be..1790b2b761f40adf11647b0b0672f2b621755fe6 100644
--- a/doc/intro/README.md
+++ b/doc/intro/README.md
@@ -22,7 +22,7 @@ Create merge requests and review code.
- [Fork a project and contribute to it](../workflow/forking_workflow.md)
- [Create a new merge request](../gitlab-basics/add-merge-request.md)
-- [Automatically close issues from merge requests](../customization/issue_closing.md)
+- [Automatically close issues from merge requests](../user/project/issues/automatic_issue_closing.md)
- [Automatically merge when your builds succeed](../user/project/merge_requests/merge_when_build_succeeds.md)
- [Revert any commit](../user/project/merge_requests/revert_changes.md)
- [Cherry-pick any commit](../user/project/merge_requests/cherry_pick_changes.md)
diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md
index 799fc15e9a3e2d4bf3fd8366bcdacc278e6f288a..91568f197f6f3ebfe8f53c3d1a6bb97f6b5db0d0 100644
--- a/doc/update/8.11-to-8.12.md
+++ b/doc/update/8.11-to-8.12.md
@@ -70,7 +70,7 @@ sudo -u git -H git checkout 8-12-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v3.5.0
+sudo -u git -H git checkout v3.6.0
```
### 6. Update gitlab-workhorse
diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md
new file mode 100644
index 0000000000000000000000000000000000000000..d30d26333de470c31b12551979b769f2c05a16d6
--- /dev/null
+++ b/doc/user/admin_area/settings/account_and_limit_settings.md
@@ -0,0 +1,42 @@
+# Account and limit settings
+
+## Repository size limit
+
+> [Introduced][ee-740] in GitLab Enterprise Edition 8.12.
+
+Repositories within your GitLab instance can grow quickly, especially if you are
+using LFS. Their size can grow exponentially and eat up your storage device quite
+quickly.
+
+In order to avoid this from happening, you can set a hard limit for your
+repositories' size. This limit can be set globally, per group, or per project,
+with per project limits taking the highest priority.
+
+Only a GitLab administrator can set those limits. Setting the limit to `0` means
+there are no restrictions.
+
+These settings can be found within each project's settings, in a group's
+settings and in the Application Settings area for the global value
+(`/admin/application_settings`).
+
+### Repository size restrictions
+
+When a project has reached its size limit, you will not be able to push to it,
+create a new merge request, or merge existing ones. You will still be able to
+create new issues, and clone the project though.
+
+Uploading LFS objects will also be denied.
+
+In order to lift these restrictions, the administrator of the GitLab instance
+needs to increase the limit on the particular project that exceeded it.
+
+### Current limitations for the repository size check
+
+The first push of a new project cannot be checked for size as of now, so the first
+push will allow you to upload more than the limit dictates, but every subsequent
+push will be denied.
+
+LFS objects, however, can be checked on first push and **will** be rejected if the
+sum of their sizes exceeds the maximum allowed repository size.
+
+[ee-740]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/740
diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md
new file mode 100644
index 0000000000000000000000000000000000000000..70dea71d3c79cdac1d522e07619fcb11e0602fe1
--- /dev/null
+++ b/doc/user/admin_area/settings/usage_statistics.md
@@ -0,0 +1,87 @@
+# Usage statistics
+
+GitLab Inc. will periodically collect information about your instance in order
+to perform various actions.
+
+All statistics are opt-in and you can always disable them from the admin panel.
+
+## Version check
+
+GitLab can inform you when an update is available and the importance of it.
+
+No information other than the GitLab version and the instance's domain name
+are collected.
+
+In the **Overview** tab you can see if your GitLab version is up to date. There
+are three cases: 1) you are up to date (green), 2) there is an update available
+(yellow) and 3) your version is vulnerable and a security fix is released (red).
+
+In any case, you will see a message informing you of the state and the
+importance of the update.
+
+If enabled, the version status will also be shown in the help page (`/help`)
+for all signed in users.
+
+## Usage data
+
+> [Introduced][ee-557] in GitLab Enterprise Edition 8.10. More statistics
+[were added][ee-735] in GitLab Enterprise Edition 8.12.
+
+GitLab Inc. can collect non-sensitive information about how Enterprise Edition
+customers use their GitLab instance upon the activation of a ping feature
+located in the admin panel (`/admin/application_settings`).
+
+You can see the **exact** JSON payload that your instance sends to GitLab Inc.
+in the "Usage statistics" section of the admin panel.
+
+Nothing qualitative is collected. Only quantitative. Meaning, no project name,
+author name, nature of comments, name of labels, etc.
+
+This is done mainly for the following reasons:
+
+- to have a better understanding on how our users use our product
+- to provide more tools for the customer success team to help customers onboard
+ better.
+
+The total number of the following is sent back to GitLab Inc.:
+
+- Comments
+- Groups
+- Users
+- Projects
+- Issues
+- Labels
+- CI builds
+- Snippets
+- Milestones
+- Todos
+- Pushes
+- Merge requests
+- Environments
+- Triggers
+- Deploy keys
+- Pages
+- Project Services
+- Issue Boards
+- CI Runners
+- Deployments
+- Geo Nodes
+- LDAP Groups
+- LDAP Keys
+- LDAP Users
+- LFS objects
+- Protected branches
+- Releases
+- Remote mirrors
+- Web hooks
+
+## Privacy policy
+
+GitLab Inc. does **not** collect any sensitive information, like project names
+or the content of the comments. GitLab Inc. does not disclose or otherwise make
+available any of the data collected on a customer specific basis.
+
+Read more in about the [Privacy policy](https://about.gitlab.com/privacy).
+
+[ee-557]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/557
+[ee-735]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/735
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 3786aeab70e555312c255ccaafb6aedba51ad7fa..08510a1bce30dea590db230907e52e5af493e468 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -141,3 +141,33 @@ instance and project. In addition, all admins can use the admin interface under
| Add shared runners | | | | ✓ |
| See events in the system | | | | ✓ |
| Admin interface | | | | ✓ |
+
+### Build permissions
+
+> Changed in GitLab 8.12.
+
+GitLab 8.12 has a completely redesigned build permissions system.
+Read all about the [new model and its implications][new-mod].
+
+This table shows granted privileges for builds triggered by specific types of
+users:
+
+| Action | Guest, Reporter | Developer | Master | Admin |
+|---------------------------------------------|-----------------|-------------|----------|--------|
+| Run CI build | | ✓ | ✓ | ✓ |
+| Clone source and LFS from current project | | ✓ | ✓ | ✓ |
+| Clone source and LFS from public projects | | ✓ | ✓ | ✓ |
+| Clone source and LFS from internal projects | | ✓ [^3] | ✓ [^3] | ✓ |
+| Clone source and LFS from private projects | | ✓ [^4] | ✓ [^4] | ✓ [^4] |
+| Push source and LFS | | | | |
+| Pull container images from current project | | ✓ | ✓ | ✓ |
+| Pull container images from public projects | | ✓ | ✓ | ✓ |
+| Pull container images from internal projects| | ✓ [^3] | ✓ [^3] | ✓ |
+| Pull container images from private projects | | ✓ [^4] | ✓ [^4] | ✓ [^4] |
+| Push container images to current project | | ✓ | ✓ | ✓ |
+| Push container images to other projects | | | | |
+
+[^3]: Only if user is not external one.
+[^4]: Only if user is a member of the project.
+[ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994
+[new-mod]: project/new_ci_build_permissions_model.md
diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md
new file mode 100644
index 0000000000000000000000000000000000000000..abef80e791417713bcc5c0a65a519c03bc7316b1
--- /dev/null
+++ b/doc/user/project/cycle_analytics.md
@@ -0,0 +1,114 @@
+# Cycle Analytics
+
+> [Introduced][ce-5986] in GitLab 8.12.
+>
+> **Note:**
+This the first iteration of Cycle Analytics, you can follow the following issue
+to track the changes that are coming to this feature: [#20975][ce-20975].
+
+Cycle Analytics measures the time it takes to go from [an idea to production] for
+each project you have. This is achieved by not only indicating the total time it
+takes to reach at that point, but the total time is broken down into the
+multiple stages an idea has to pass through to be shipped.
+
+Cycle Analytics is that it is tightly coupled with the [GitLab flow] and
+calculates a separate median for each stage.
+
+## Overview
+
+You can find the Cycle Analytics page under your project's **Pipelines > Cycle
+Analytics** tab.
+
+
+
+You can see that there are seven stages in total:
+
+- **Issue** (Tracker)
+ - Median time from issue creation until given a milestone or list label
+ (first assignment, any milestone, milestone date or assignee is not required)
+- **Plan** (Board)
+ - Median time from giving an issue a milestone or label until pushing the
+ first commit
+- **Code** (IDE)
+ - Median time from the first commit until the merge request is created
+- **Test** (CI)
+ - Median total test time for all commits/merges
+- **Review** (Merge Request/MR)
+ - Median time from merge request creation until the merge request is merged
+ (closed merge requests won't be taken into account)
+- **Staging** (Continuous Deployment)
+ - Median time from when the merge request got merged until the deploy to
+ production (production is last stage/environment)
+- **Production** (Total)
+ - Sum of all the above stages excluding the Test (CI) time
+
+## How the data is measured
+
+Cycle Analytics records cycle time so only data on the issues that have been
+deployed to production are measured. In case you just started a new project and
+you have not pushed anything to production, then you will not be able to
+properly see the Cycle Analytics of your project.
+
+Specifically, if your CI is not set up and you have not defined a `production`
+[environment], then you will not have any data.
+
+Below you can see in more detail what the various stages of Cycle Analytics mean.
+
+| **Stage** | **Description** |
+| --------- | --------------- |
+| Issue | Measures the median time between creating an issue and taking action to solve it, by either labeling it or adding it to a milestone, whatever comes first. The label will be tracked only if it already has an [Issue Board list][board] created for it. |
+| Plan | Measures the median time between the action you took for the previous stage, and pushing the first commit to the repository. To make this change tracked, the pushed commit needs to contain the [issue closing pattern], for example `Closes #xxx`, where `xxx` is the number of the issue related to this commit. If the commit does not contain the issue closing pattern, it is not considered to the measurement time of the stage. |
+| Code | Measures the median time between pushing a first commit (previous stage) and creating a merge request related to that commit. The key to keep the process tracked is include the [issue closing pattern] to the description of the merge request. |
+| Test | Measures the median time to run the entire pipeline for that project. It's related to the time GitLab CI takes to run every job for the commits pushed to that merge request defined in the previous stage. It is basically the start->finish time for all pipelines. `master` is not excluded. It does not attempt to track time for any particular stages. |
+| Review | Measures the median time taken to review the merge request, between its creation and until it's merged. |
+| Staging | Measures the median time between merging the merge request until the very first deployment to production. It's tracked by the [environment] set to `production` in your GitLab CI configuration. If there isn't a `production` environment, this is not tracked. |
+| Production| The sum of all time taken to run the entire process, from issue creation to deploying the code to production. |
+
+---
+
+Here's a little explanation of how this works behind the scenes:
+
+1. Issues and merge requests are grouped together in pairs, such that for each
+ `` pair, the merge request has `Fixes #xxx` for the
+ corresponding issue. All other issues and merge requests are **not** considered.
+
+1. Then the pairs are filtered out. Any merge request
+ that has **not** been deployed to production in the last XX days (specified
+ by the UI - default is 90 days) prohibits these pairs from being considered.
+
+1. For the remaining `` pairs, we check the information that
+ we need for the stages, like issue creation date, merge request merge time,
+ etc.
+
+To sum up, anything that doesn't follow the [GitLab flow] won't be tracked at all.
+So, if a merge request doesn't close an issue or an issue is not labeled with a
+label present in the Issue Board or assigned a milestone or a project has no
+`production` environment, the Cycle Analytics dashboard won't present any data
+at all.
+
+## Permissions
+
+The current permissions on the Cycle Analytics dashboard are:
+
+- Public projects - anyone can access
+- Private/internal projects - any member (guest level and above) can access
+
+You can [read more about permissions][permissions] in general.
+
+## More resources
+
+Learn more about Cycle Analytics in the following resources:
+
+- [Cycle Analytics feature page](https://about.gitlab.com/solutions/cycle-analytics/)
+- [Cycle Analytics feature preview](https://about.gitlab.com/2016/09/16/feature-preview-introducing-cycle-analytics/)
+- [Cycle Analytics feature highlight](https://about.gitlab.com/2016/09/21/cycle-analytics-feature-highlight/)
+
+
+[ce-5986]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5986
+[ce-20975]: https://gitlab.com/gitlab-org/gitlab-ce/issues/20975
+[GitLab flow]: ../../workflow/gitlab_flow.md
+[permissions]: ../permissions.md
+[environment]: ../../ci/yaml/README.md#environment
+[board]: issue_board.md#creating-a-new-list
+[idea to production]: https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab
+[issue closing pattern]: issues/automatic_issue_closing.md
diff --git a/doc/user/project/img/cycle_analytics_landing_page.png b/doc/user/project/img/cycle_analytics_landing_page.png
new file mode 100644
index 0000000000000000000000000000000000000000..4fa42c87395458739b63cf23eb017cc0cd400db0
Binary files /dev/null and b/doc/user/project/img/cycle_analytics_landing_page.png differ
diff --git a/doc/user/project/issues/automatic_issue_closing.md b/doc/user/project/issues/automatic_issue_closing.md
new file mode 100644
index 0000000000000000000000000000000000000000..d6f3a7d5555f5e444ab5806a958b0adebd9321f1
--- /dev/null
+++ b/doc/user/project/issues/automatic_issue_closing.md
@@ -0,0 +1,55 @@
+# Automatic issue closing
+
+>**Note:**
+This is the user docs. In order to change the default issue closing pattern,
+follow the steps in the [administration docs].
+
+When a commit or merge request resolves one or more issues, it is possible to
+automatically have these issues closed when the commit or merge request lands
+in the project's default branch.
+
+If a commit message or merge request description contains a sentence matching
+a certain regular expression, all issues referenced from the matched text will
+be closed. This happens when the commit is pushed to a project's **default**
+branch, or when a commit or merge request is merged into it.
+
+## Default closing pattern value
+
+When not specified, the default issue closing pattern as shown below will be
+used:
+
+```bash
+((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+)
+```
+
+Note that `%{issue_ref}` is a complex regular expression defined inside GitLab's
+source code that can match a reference to 1) a local issue (`#123`),
+2) a cross-project issue (`group/project#123`) or 3) a link to an issue
+(`https://gitlab.example.com/group/project/issues/123`).
+
+---
+
+This translates to the following keywords:
+
+- Close, Closes, Closed, Closing, close, closes, closed, closing
+- Fix, Fixes, Fixed, Fixing, fix, fixes, fixed, fixing
+- Resolve, Resolves, Resolved, Resolving, resolve, resolves, resolved, resolving
+
+---
+
+For example the following commit message:
+
+```
+Awesome commit message
+
+Fix #20, Fixes #21 and Closes group/otherproject#22.
+This commit is also related to #17 and fixes #18, #19
+and https://gitlab.example.com/group/otherproject/issues/23.
+```
+
+will close `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed
+to, as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as
+it does not match the pattern. It works with multi-line commit messages as well
+as one-liners when used with `git commit -m`.
+
+[administration docs]: ../../../administration/issue_closing_pattern.md
diff --git a/doc/user/project/merge_requests/img/versions-compare.png b/doc/user/project/merge_requests/img/versions-compare.png
new file mode 100644
index 0000000000000000000000000000000000000000..890cae7768cc9fc0814855963ffd9d914ad4c577
Binary files /dev/null and b/doc/user/project/merge_requests/img/versions-compare.png differ
diff --git a/doc/user/project/merge_requests/img/versions-dropdown.png b/doc/user/project/merge_requests/img/versions-dropdown.png
new file mode 100644
index 0000000000000000000000000000000000000000..9bab9304e147aa4bbc7fafd022bd18e78ecffa72
Binary files /dev/null and b/doc/user/project/merge_requests/img/versions-dropdown.png differ
diff --git a/doc/user/project/merge_requests/img/versions.png b/doc/user/project/merge_requests/img/versions.png
index f279ccd7ce31b31182c409a449ec142e706a9e39..6c86f2c68ac59643f47938afa344f00c5bb50f18 100644
Binary files a/doc/user/project/merge_requests/img/versions.png and b/doc/user/project/merge_requests/img/versions.png differ
diff --git a/doc/user/project/merge_requests/versions.md b/doc/user/project/merge_requests/versions.md
index a6aa4b47835cb3be86bce7ee9c2f7cca7b97188d..2805fdf635c49de11a2429747483186b397825fc 100644
--- a/doc/user/project/merge_requests/versions.md
+++ b/doc/user/project/merge_requests/versions.md
@@ -7,14 +7,18 @@ of merge request diff is created. When you visit a merge request that contains
more than one pushes, you can select and compare the versions of those merge
request diffs.
+
+
By default, the latest version of changes is shown. However, you
can select an older one from version dropdown.
-
+
You can also compare the merge request version with older one to see what is
changed since then.
+
+
Please note that comments are disabled while viewing outdated merge versions
or comparing to versions other than base.
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
new file mode 100644
index 0000000000000000000000000000000000000000..e73f60023b5fbed7dc707472b70fb1d792c0cc3e
--- /dev/null
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -0,0 +1,289 @@
+# New CI build permissions model
+
+> Introduced in GitLab 8.12.
+
+GitLab 8.12 has a completely redesigned [build permissions] system. You can find
+all discussion and all our concerns when choosing the current approach in issue
+[#18994](https://gitlab.com/gitlab-org/gitlab-ce/issues/18994).
+
+---
+
+Builds permissions should be tightly integrated with the permissions of a user
+who is triggering a build.
+
+The reasons to do it like that are:
+
+- We already have a permissions system in place: group and project membership
+ of users.
+- We already fully know who is triggering a build (using `git push`, using the
+ web UI, executing triggers).
+- We already know what user is allowed to do.
+- We use the user permissions for builds that are triggered by the user.
+- It opens a lot of possibilities to further enforce user permissions, like
+ allowing only specific users to access runners or use secure variables and
+ environments.
+- It is simple and convenient that your build can access everything that you
+ as a user have access to.
+- Short living unique tokens are now used, granting access for time of the build
+ and maximizing security.
+
+With the new behavior, any build that is triggered by the user, is also marked
+with their permissions. When a user does a `git push` or changes files through
+the web UI, a new pipeline will be usually created. This pipeline will be marked
+as created be the pusher (local push or via the UI) and any build created in this
+pipeline will have the permissions of the pusher.
+
+This allows us to make it really easy to evaluate the access for all projects
+that have Git submodules or are using container images that the pusher would
+have access too. **The permission is granted only for time that build is running.
+The access is revoked after the build is finished.**
+
+## Types of users
+
+It is important to note that we have a few types of users:
+
+- **Administrators**: CI builds created by Administrators will not have access
+ to all GitLab projects, but only to projects and container images of projects
+ that the administrator is a member of.That means that if a project is either
+ public or internal users have access anyway, but if a project is private, the
+ Administrator will have to be a member of it in order to have access to it
+ via another project's build.
+
+- **External users**: CI builds created by [external users][ext] will have
+ access only to projects to which user has at least reporter access. This
+ rules out accessing all internal projects by default,
+
+This allows us to make the CI and permission system more trustworthy.
+Let's consider the following scenario:
+
+1. You are an employee of a company. Your company has a number of internal tools
+ hosted in private repositories and you have multiple CI builds that make use
+ of these repositories.
+
+2. You invite a new [external user][ext]. CI builds created by that user do not
+ have access to internal repositories, because the user also doesn't have the
+ access from within GitLab. You as an employee have to grant explicit access
+ for this user. This allows us to prevent from accidental data leakage.
+
+## Build token
+
+A unique build token is generated for each build and it allows the user to
+access all projects that would be normally accessible to the user creating that
+build.
+
+We try to make sure that this token doesn't leak by:
+
+1. Securing all API endpoints to not expose the build token.
+1. Masking the build token from build logs.
+1. Allowing to use the build token **only** when build is running.
+
+However, this brings a question about the Runners security. To make sure that
+this token doesn't leak, you should also make sure that you configure
+your Runners in the most possible secure way, by avoiding the following:
+
+1. Any usage of Docker's `privileged` mode is risky if the machines are re-used.
+1. Using the `shell` executor since builds run on the same machine.
+
+By using an insecure GitLab Runner configuration, you allow the rogue developers
+to steal the tokens of other builds.
+
+## Debugging problems
+
+With the new permission model in place, there may be times that your build will
+fail. This is most likely because your project tries to access other project's
+sources, and you don't have the appropriate permissions. In the build log look
+for information about 403 or forbidden access messages
+
+As an Administrator, you can verify that the user is a member of the group or
+project they're trying to have access to, and you can impersonate the user to
+retry the failing build in order to verify that everything is correct.
+
+## Build triggers
+
+[Build triggers][triggers] do not support the new permission model.
+They continue to use the old authentication mechanism where the CI build
+can access only its own sources. We plan to remove that limitation in one of
+the upcoming releases.
+
+## Before GitLab 8.12
+
+In versions before GitLab 8.12, all CI builds would use the CI Runner's token
+to checkout project sources.
+
+The project's Runner's token was a token that you could find under the
+project's **Settings > CI/CD Pipelines** and was limited to access only that
+project.
+It could be used for registering new specific Runners assigned to the project
+and to checkout project sources.
+It could also be used with the GitLab Container Registry for that project,
+allowing pulling and pushing Docker images from within the CI build.
+
+---
+
+GitLab would create a special checkout URL like:
+
+```
+https://gitlab-ci-token:/gitlab.com/gitlab-org/gitlab-ce.git
+```
+
+And then the users could also use it in their CI builds all Docker related
+commands to interact with GitLab Container Registry. For example:
+
+```
+docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.gitlab.com
+```
+
+Using single token had multiple security implications:
+
+- The token would be readable to anyone who had developer access to a project
+ that could run CI builds, allowing the developer to register any specific
+ Runner for that project.
+- The token would allow to access only the project's sources, forbidding from
+ accessing any other projects.
+- The token was not expiring and was multi-purpose: used for checking out sources,
+ for registering specific runners and for accessing a project's container
+ registry with read-write permissions.
+
+All the above led to a new permission model for builds that was introduced
+with GitLab 8.12.
+
+## Making use of the new CI build permissions model
+
+With the new build permission model, there is now an easy way to access all
+dependent source code in a project. That way, we can:
+
+1. Access a project's Git submodules
+1. Access private container images
+1. Access project's and submodule LFS objects
+
+Let's see how that works with Git submodules and private Docker images hosted on
+the container registry.
+
+## Git submodules
+
+>
+It often happens that while working on one project, you need to use another
+project from within it; perhaps it’s a library that a third party developed or
+you’re developing a project separately and are using it in multiple parent
+projects.
+A common issue arises in these scenarios: you want to be able to treat the two
+projects as separate yet still be able to use one from within the other.
+>
+_Excerpt from the [Git website][git-scm] about submodules._
+
+If dealing with submodules, your project will probably have a file named
+`.gitmodules`. And this is how it usually looks like:
+
+```
+[submodule "tools"]
+ path = tools
+ url = git@gitlab.com/group/tools.git
+```
+
+> **Note:**
+If you are **not** using GitLab 8.12 or higher, you would need to work your way
+around this issue in order to access the sources of `gitlab.com/group/tools`
+(e.g., use [SSH keys](../ssh_keys/README.md)).
+>
+With GitLab 8.12 onward, your permissions are used to evaluate what a CI build
+can access. More information about how this system works can be found in the
+[Build permissions model](../../user/permissions.md#builds-permissions).
+
+To make use of the new changes, you have to update your `.gitmodules` file to
+use a relative URL.
+
+Let's consider the following example:
+
+1. Your project is located at `https://gitlab.com/secret-group/my-project`.
+1. To checkout your sources you usually use an SSH address like
+ `git@gitlab.com:secret-group/my-project.git`.
+1. Your project depends on `https://gitlab.com/group/tools`.
+1. You have the `.gitmodules` file with above content.
+
+Since Git allows the usage of relative URLs for your `.gitmodules` configuration,
+this easily allows you to use HTTP for cloning all your CI builds and SSH
+for all your local checkouts.
+
+For example, if you change the `url` of your `tools` dependency, from
+`git@gitlab.com/group/tools.git` to `../../group/tools.git`, this will instruct
+Git to automatically deduce the URL that should be used when cloning sources.
+Whether you use HTTP or SSH, Git will use that same channel and it will allow
+to make all your CI builds use HTTPS (because GitLab CI uses HTTPS for cloning
+your sources), and all your local clones will continue using SSH.
+
+Given the above explanation, your `.gitmodules` file should eventually look
+like this:
+
+```
+[submodule "tools"]
+ path = tools
+ url = ../../group/tools.git
+```
+
+However, you have to explicitly tell GitLab CI to clone your submodules as this
+is not done automatically. You can achieve that by adding a `before_script`
+section to your `.gitlab-ci.yml`:
+
+```
+before_script:
+ - git submodule update --init --recursive
+
+test:
+ script:
+ - run-my-tests
+```
+
+This will make GitLab CI initialize (fetch) and update (checkout) all your
+submodules recursively.
+
+In case your environment or your Docker image doesn't have Git installed,
+you have to either ask your Administrator or install the missing dependency
+yourself:
+
+```
+# Debian / Ubuntu
+before_script:
+ - apt-get update -y
+ - apt-get install -y git-core
+ - git submodule update --init --recursive
+
+# CentOS / RedHat
+before_script:
+ - yum install git
+ - git submodule update --init --recursive
+
+# Alpine
+before_script:
+ - apk add -U git
+ - git submodule update --init --recursive
+```
+
+### Container Registry
+
+With the update permission model we also extended the support for accessing
+Container Registries for private projects.
+
+> **Note:**
+As GitLab Runner 1.6 doesn't yet incorporate the introduced changes for
+permissions, this makes the `image:` directive to not work with private projects
+automatically. The manual configuration by an Administrator is required to use
+private images. We plan to remove that limitation in one of the upcoming releases.
+
+Your builds can access all container images that you would normally have access
+to. The only implication is that you can push to the Container Registry of the
+project for which the build is triggered.
+
+This is how an example usage can look like:
+
+```
+test:
+ script:
+ - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
+ - docker pull $CI_REGISTRY/group/other-project:latest
+ - docker run $CI_REGISTRY/group/other-project:latest
+```
+
+[git-scm]: https://git-scm.com/book/en/v2/Git-Tools-Submodules
+[build permissions]: ../permissions.md#builds-permissions
+[ext]: ../permissions.md#external-users
+[triggers]: ../../ci/triggers/README.md
diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md
index 7c041d019bb4299bebe8256f64ee9074b8caee18..993c6bfb7e95accce66b1567c1172df39dd1a4b5 100644
--- a/doc/user/project/repository/web_editor.md
+++ b/doc/user/project/repository/web_editor.md
@@ -172,4 +172,4 @@ you commit the changes you will be taken to a new merge request form.

[ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808
-[issue closing pattern]: ../customization/issue_closing.md
+[issue closing pattern]: ../user/project/issues/automatic_issue_closing.md
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index d2d703ed5ff1bbfe3e64d929ca5f7dfcecbd265b..f922bcf28972d3155f17e8be1d5e869c609e7b8a 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -1,6 +1,8 @@
# Workflow
+- [Automatic issue closing](../user/project/issues/automatic_issue_closing.md)
- [Change your time zone](timezone.md)
+- [Cycle Analytics](../user/project/cycle_analytics.md)
- [Description templates](../user/project/description_templates.md)
- [Feature branch workflow](workflow.md)
- [GitLab Flow](gitlab_flow.md)
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index 7c22b17e4e507a9b5397989a05c2e19d668a21d6..2461a783ea83abf654f16aaa705047c3bb14d05f 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -1,12 +1,12 @@
module API
class AwardEmoji < Grape::API
before { authenticate! }
- AWARDABLES = [Issue, MergeRequest]
+ AWARDABLES = %w[issue merge_request snippet]
resource :projects do
AWARDABLES.each do |awardable_type|
- awardable_string = awardable_type.to_s.underscore.pluralize
- awardable_id_string = "#{awardable_type.to_s.underscore}_id"
+ awardable_string = awardable_type.pluralize
+ awardable_id_string = "#{awardable_type}_id"
[ ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji",
":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji"
@@ -87,9 +87,7 @@ class AwardEmoji < Grape::API
helpers do
def can_read_awardable?
- ability = "read_#{awardable.class.to_s.underscore}".to_sym
-
- can?(current_user, ability, awardable)
+ can?(current_user, read_ability(awardable), awardable)
end
def can_award_awardable?
@@ -100,18 +98,25 @@ def awardable
@awardable ||=
begin
if params.include?(:note_id)
- noteable.notes.find(params[:note_id])
+ note_id = params.delete(:note_id)
+
+ awardable.notes.find(note_id)
+ elsif params.include?(:issue_id)
+ user_project.issues.find(params[:issue_id])
+ elsif params.include?(:merge_request_id)
+ user_project.merge_requests.find(params[:merge_request_id])
else
- noteable
+ user_project.snippets.find(params[:snippet_id])
end
end
end
- def noteable
- if params.include?(:issue_id)
- user_project.issues.find(params[:issue_id])
+ def read_ability(awardable)
+ case awardable
+ when Note
+ read_ability(awardable.noteable)
else
- user_project.merge_requests.find(params[:merge_request_id])
+ :"read_#{awardable.class.to_s.underscore}"
end
end
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 49fc665bf74c77576db8d95b36f0679e1b9d16c6..a741f53e486502b4c8a1ea24414941395ff79c32 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -211,7 +211,9 @@ def map_public_to_visibility_level(attrs)
if namespace_id.present?
namespace = Namespace.find_by(id: namespace_id) || Namespace.find_by_path_or_name(namespace_id)
- not_found!('Target Namespace') unless namespace
+ unless namespace && can?(current_user, :create_projects, namespace)
+ not_found!('Target Namespace')
+ end
attrs[:namespace] = namespace
end
diff --git a/lib/ci/mask_secret.rb b/lib/ci/mask_secret.rb
index 3da04edde70bcbf3a441b660826b90b48a6a56ea..997377abc55fd6ff858298319dd36a342469994f 100644
--- a/lib/ci/mask_secret.rb
+++ b/lib/ci/mask_secret.rb
@@ -1,9 +1,10 @@
module Ci::MaskSecret
class << self
- def mask(value, token)
+ def mask!(value, token)
return value unless value.present? && token.present?
- value.gsub(token, 'x' * token.length)
+ value.gsub!(token, 'x' * token.length)
+ value
end
end
end
diff --git a/lib/ee/gitlab/deltas.rb b/lib/ee/gitlab/deltas.rb
new file mode 100644
index 0000000000000000000000000000000000000000..deda13dc59df93e7d3aceeb0fc832bb45dedf13b
--- /dev/null
+++ b/lib/ee/gitlab/deltas.rb
@@ -0,0 +1,25 @@
+module EE
+ module Gitlab
+ module Deltas
+ def self.delta_size_check(change, repo)
+ size_of_deltas = 0
+
+ begin
+ tree_a = repo.lookup(change[:oldrev])
+ tree_b = repo.lookup(change[:newrev])
+ diff = tree_a.diff(tree_b)
+
+ diff.each_delta do |d|
+ new_file_size = d.deleted? ? 0 : ::Gitlab::Git::Blob.raw(repo, d.new_file[:oid]).size
+
+ size_of_deltas += new_file_size
+ end
+
+ size_of_deltas
+ rescue Rugged::OdbError, Rugged::ReferenceError, Rugged::InvalidError
+ size_of_deltas
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ee/gitlab/ldap/adapter.rb b/lib/ee/gitlab/ldap/adapter.rb
index a2aa35e25063e9c6e73215797cdec6738e45a528..0124285dd7f19b10d147c8632330aa0e6942b9a3 100644
--- a/lib/ee/gitlab/ldap/adapter.rb
+++ b/lib/ee/gitlab/ldap/adapter.rb
@@ -1,7 +1,7 @@
# LDAP connection adapter EE mixin
#
# This module is intended to encapsulate EE-specific adapter methods
-# and be included in the `Gitlab::LDAP::Adapter` class.
+# and be **prepended** in the `Gitlab::LDAP::Adapter` class.
module EE
module Gitlab
module LDAP
@@ -31,13 +31,32 @@ def group(*args)
groups(*args).first
end
- def dns_for_filter(filter)
+ def group_members_in_range(dn, range_start)
ldap_search(
- base: config.base,
- filter: filter,
- scope: Net::LDAP::SearchScope_WholeSubtree,
- attributes: %w{dn}
- ).map(&:dn)
+ base: dn,
+ scope: Net::LDAP::SearchScope_BaseObject,
+ attributes: ["member;range=#{range_start}-*"],
+ ).first
+ end
+
+ def nested_groups(parent_dn)
+ options = {
+ base: config.group_base,
+ filter: Net::LDAP::Filter.join(
+ Net::LDAP::Filter.eq('objectClass', 'group'),
+ Net::LDAP::Filter.eq('memberOf', parent_dn)
+ )
+ }
+
+ ldap_search(options).map do |entry|
+ LDAP::Group.new(entry, self)
+ end
+ end
+
+ def user_attributes
+ attributes = super
+ attributes << config.sync_ssh_keys if config.sync_ssh_keys
+ attributes
end
end
end
diff --git a/lib/ee/gitlab/ldap/group.rb b/lib/ee/gitlab/ldap/group.rb
index 6ed5a548cbd6eef7716e504fc67ec915cae47b3b..09a390fa8ae19434e65d09ef45a0b5e909c654a1 100644
--- a/lib/ee/gitlab/ldap/group.rb
+++ b/lib/ee/gitlab/ldap/group.rb
@@ -39,14 +39,15 @@ def member_uids
entry.memberuid
end
- def member_dns
+ def dn
+ entry.dn
+ end
+
+ def member_dns(nested_groups_to_skip = [])
dns = []
- # There's an edge-case with AD where sometimes a recursive search
- # doesn't return all users at the top-level. Concat recursive results
- # with regular results to be safe. See gitlab-ee#484
- if active_directory?
- dns = adapter.dns_for_filter(active_directory_recursive_memberof_filter)
+ if active_directory? && adapter
+ dns.concat(active_directory_members(entry, nested_groups_to_skip))
end
if (entry.respond_to? :member) && (entry.respond_to? :submember)
@@ -60,20 +61,91 @@ def member_dns
else
Rails.logger.warn("Could not find member DNs for LDAP group #{entry.inspect}")
end
+
dns.uniq
end
private
- # We use the ActiveDirectory LDAP_MATCHING_RULE_IN_CHAIN matching rule; see
- # http://msdn.microsoft.com/en-us/library/aa746475%28VS.85%29.aspx#code-snippet-5
- def active_directory_recursive_memberof_filter
- Net::LDAP::Filter.ex("memberOf:1.2.840.113556.1.4.1941", entry.dn)
- end
-
def entry
@entry
end
+
+ # Active Directory range member methods
+
+ def has_member_range?(entry)
+ member_range_attribute(entry).present?
+ end
+
+ def member_range_attribute(entry)
+ entry.attribute_names.find { |a| a.to_s.start_with?("member;range=")}.to_s
+ end
+
+ def active_directory_members(entry, nested_groups_to_skip)
+ require 'net/ldap/dn'
+
+ members = []
+
+ # Retrieve all member pages/ranges
+ members.concat(ranged_members(entry)) if has_member_range?(entry)
+ # Process nested group members
+ members.concat(nested_members(nested_groups_to_skip))
+
+ # Clean dns of groups and users outside the base
+ members.reject! { |dn| nested_groups_to_skip.include?(dn) }
+ base = Net::LDAP::DN.new(adapter.config.base.downcase).to_a
+ members.select! { |dn| Net::LDAP::DN.new(dn.downcase).to_a.last(base.length) == base }
+
+ members
+ end
+
+ # AD requires use of range retrieval for groups with more than 1500 members
+ # cf. https://msdn.microsoft.com/en-us/library/aa367017(v=vs.85).aspx
+ def ranged_members(entry)
+ members = []
+
+ # Concatenate the members in the current range
+ members.concat(entry[member_range_attribute(entry)])
+
+ # Recursively concatenate members until end of ranges
+ if has_more_member_ranges?(entry)
+ next_entry = adapter.group_members_in_range(dn, next_member_range_start(entry))
+
+ members.concat(ranged_members(next_entry))
+ end
+
+ members
+ end
+
+ # Process any AD nested groups. Use a manual process because
+ # the AD recursive member of filter is too slow and uses too
+ # much CPU on the AD server.
+ def nested_members(nested_groups_to_skip)
+ # Ignore this group if we see it again in a nested group.
+ # Prevents infinite loops.
+ nested_groups_to_skip << dn
+
+ members = []
+ nested_groups = adapter.nested_groups(dn)
+
+ nested_groups.each do |nested_group|
+ next if nested_groups_to_skip.include?(nested_group.dn)
+
+ members.concat(nested_group.member_dns(nested_groups_to_skip))
+ end
+
+ members
+ end
+
+ def has_more_member_ranges?(entry)
+ next_member_range_start(entry).present?
+ end
+
+ def next_member_range_start(entry)
+ match = member_range_attribute(entry).match /^member;range=\d+-(\d+|\*)$/
+
+ match[1].to_i + 1 if match.present? && match[1] != '*'
+ end
end
end
end
diff --git a/lib/gitlab/database/date_time.rb b/lib/gitlab/database/date_time.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b6a89f715fdaaa725a5546682d165c2beac28128
--- /dev/null
+++ b/lib/gitlab/database/date_time.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Database
+ module DateTime
+ # Find the first of the `end_time_attrs` that isn't `NULL`. Subtract from it
+ # the first of the `start_time_attrs` that isn't NULL. `SELECT` the resulting interval
+ # along with an alias specified by the `as` parameter.
+ #
+ # Note: For MySQL, the interval is returned in seconds.
+ # For PostgreSQL, the interval is returned as an INTERVAL type.
+ def subtract_datetimes(query_so_far, end_time_attrs, start_time_attrs, as)
+ diff_fn = if Gitlab::Database.postgresql?
+ Arel::Nodes::Subtraction.new(
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(end_time_attrs)),
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(start_time_attrs)))
+ elsif Gitlab::Database.mysql?
+ Arel::Nodes::NamedFunction.new(
+ "TIMESTAMPDIFF",
+ [Arel.sql('second'),
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(start_time_attrs)),
+ Arel::Nodes::NamedFunction.new("COALESCE", Array.wrap(end_time_attrs))])
+ end
+
+ query_so_far.project(diff_fn.as(as))
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1444d25ebc7563540ded9cd95846158ad369a349
--- /dev/null
+++ b/lib/gitlab/database/median.rb
@@ -0,0 +1,112 @@
+# https://www.periscopedata.com/blog/medians-in-sql.html
+module Gitlab
+ module Database
+ module Median
+ def median_datetime(arel_table, query_so_far, column_sym)
+ median_queries =
+ if Gitlab::Database.postgresql?
+ pg_median_datetime_sql(arel_table, query_so_far, column_sym)
+ elsif Gitlab::Database.mysql?
+ mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
+ end
+
+ results = Array.wrap(median_queries).map do |query|
+ ActiveRecord::Base.connection.execute(query)
+ end
+ extract_median(results).presence
+ end
+
+ def extract_median(results)
+ result = results.compact.first
+
+ if Gitlab::Database.postgresql?
+ result = result.first.presence
+ median = result['median'] if result
+ median.to_f if median
+ elsif Gitlab::Database.mysql?
+ result.to_a.flatten.first
+ end
+ end
+
+ def mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
+ query = arel_table.
+ from(arel_table.project(Arel.sql('*')).order(arel_table[column_sym]).as(arel_table.table_name)).
+ project(average([arel_table[column_sym]], 'median')).
+ where(
+ Arel::Nodes::Between.new(
+ Arel.sql("(select @row_id := @row_id + 1)"),
+ Arel::Nodes::And.new(
+ [Arel.sql('@ct/2.0'),
+ Arel.sql('@ct/2.0 + 1')]
+ )
+ )
+ ).
+ # Disallow negative values
+ where(arel_table[column_sym].gteq(0))
+
+ [
+ Arel.sql("CREATE TEMPORARY TABLE IF NOT EXISTS #{query_so_far.to_sql}"),
+ Arel.sql("set @ct := (select count(1) from #{arel_table.table_name});"),
+ Arel.sql("set @row_id := 0;"),
+ query.to_sql,
+ Arel.sql("DROP TEMPORARY TABLE IF EXISTS #{arel_table.table_name};")
+ ]
+ end
+
+ def pg_median_datetime_sql(arel_table, query_so_far, column_sym)
+ # Create a CTE with the column we're operating on, row number (after sorting by the column
+ # we're operating on), and count of the table we're operating on (duplicated across) all rows
+ # of the CTE. For example, if we're looking to find the median of the `projects.star_count`
+ # column, the CTE might look like this:
+ #
+ # star_count | row_id | ct
+ # ------------+--------+----
+ # 5 | 1 | 3
+ # 9 | 2 | 3
+ # 15 | 3 | 3
+ cte_table = Arel::Table.new("ordered_records")
+ cte = Arel::Nodes::As.new(
+ cte_table,
+ arel_table.
+ project(
+ arel_table[column_sym].as(column_sym.to_s),
+ Arel::Nodes::Over.new(Arel::Nodes::NamedFunction.new("row_number", []),
+ Arel::Nodes::Window.new.order(arel_table[column_sym])).as('row_id'),
+ arel_table.project("COUNT(1)").as('ct')).
+ # Disallow negative values
+ where(arel_table[column_sym].gteq(zero_interval)))
+
+ # From the CTE, select either the middle row or the middle two rows (this is accomplished
+ # by 'where cte.row_id between cte.ct / 2.0 AND cte.ct / 2.0 + 1'). Find the average of the
+ # selected rows, and this is the median value.
+ cte_table.project(average([extract_epoch(cte_table[column_sym])], "median")).
+ where(
+ Arel::Nodes::Between.new(
+ cte_table[:row_id],
+ Arel::Nodes::And.new(
+ [(cte_table[:ct] / Arel.sql('2.0')),
+ (cte_table[:ct] / Arel.sql('2.0') + 1)]
+ )
+ )
+ ).
+ with(query_so_far, cte).
+ to_sql
+ end
+
+ private
+
+ def average(args, as)
+ Arel::Nodes::NamedFunction.new("AVG", args, as)
+ end
+
+ def extract_epoch(arel_attribute)
+ Arel.sql(%Q{EXTRACT(EPOCH FROM "#{arel_attribute.relation.name}"."#{arel_attribute.name}")})
+ end
+
+ # Need to cast '0' to an INTERVAL before we can check if the interval is positive
+ def zero_interval
+ Arel::Nodes::NamedFunction.new("CAST", [Arel.sql("'0' AS INTERVAL")])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index 3ab99360206f909fac522874792a6189d746f0f3..3cd515e4a3ab742fedafa36320aadee1167fa1fc 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -19,6 +19,8 @@ def branch_name(ref)
end
def committer_hash(email:, name:)
+ return if email.nil? || name.nil?
+
{
email: email,
name: name,
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 8ece7c1491e83a8747c98627da8e081fc4f2f06b..fb6c4fbbb231f83e9c7b63808cb9c41dde4b7370 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -104,9 +104,9 @@ def user_push_access_check(changes)
return build_status_object(true)
end
- unless project.repository.exists?
- return build_status_object(false, "A repository for this project does not exist yet.")
- end
+ return build_status_object(false, "A repository for this project does not exist yet.") unless project.repository.exists?
+
+ return build_status_object(false, Gitlab::RepositorySizeError.new(project).push_error) if project.above_size_limit?
if ::License.block_changes?
message = ::LicenseHelper.license_message(signed_in: true, is_admin: (user && user.is_admin?))
@@ -115,6 +115,8 @@ def user_push_access_check(changes)
changes_list = Gitlab::ChangesList.new(changes)
+ push_size_in_bytes = 0
+
# Iterate over all changes to find if user allowed all of them to be applied
changes_list.each do |change|
status = change_access_check(change)
@@ -122,6 +124,14 @@ def user_push_access_check(changes)
# If user does not have access to make at least one change - cancel all push
return status
end
+
+ if project.size_limit_enabled?
+ push_size_in_bytes += EE::Gitlab::Deltas.delta_size_check(change, project.repository)
+ end
+ end
+
+ if project.changes_will_exceed_size_limit?(push_size_in_bytes.to_mb)
+ return build_status_object(false, Gitlab::RepositorySizeError.new(project).new_changes_error)
end
build_status_object(true)
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 925a952156ff151a7cb4a833b55c502de3bf0379..88803d766234852894ddfc95b6d037e8f671ed35 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -10,6 +10,7 @@ project_tree:
- milestone:
- :events
- snippets:
+ - :award_emoji
- notes:
:author
- :releases
@@ -66,6 +67,8 @@ excluded_attributes:
- :milestone_id
merge_requests:
- :milestone_id
+ award_emoji:
+ - :awardable_id
methods:
statuses:
diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb
index 68f61fa293caa353dcbdef98c259b6f93bfaf1a7..3f84cd816c8018255b7880bffad1994532aa2f70 100644
--- a/lib/gitlab/ldap/adapter.rb
+++ b/lib/gitlab/ldap/adapter.rb
@@ -5,7 +5,7 @@
module Gitlab
module LDAP
class Adapter
- include EE::Gitlab::LDAP::Adapter
+ prepend EE::Gitlab::LDAP::Adapter
attr_reader :provider, :ldap
@@ -76,7 +76,7 @@ def ldap_search(*args)
private
def user_options(field, value, limit)
- options = { attributes: %W(#{config.uid} cn mail dn) }
+ options = { attributes: user_attributes }
options[:size] = limit if limit
if field.to_sym == :dn
@@ -104,6 +104,10 @@ def user_filter(filter = nil)
filter
end
end
+
+ def user_attributes
+ %W(#{config.uid} cn mail dn)
+ end
end
end
end
diff --git a/lib/gitlab/repository_size_error.rb b/lib/gitlab/repository_size_error.rb
new file mode 100644
index 0000000000000000000000000000000000000000..91f8a6a3cb1f30a2c385a9a48bbd5c916c14c451
--- /dev/null
+++ b/lib/gitlab/repository_size_error.rb
@@ -0,0 +1,57 @@
+module Gitlab
+ class RepositorySizeError < StandardError
+ include ActionView::Helpers
+
+ attr_reader :project
+
+ def initialize(project)
+ @project = project
+ end
+
+ def to_s
+ "The size of this repository (#{current_size}) exceeds the limit of #{limit} by #{size_to_remove}."
+ end
+
+ def commit_error
+ "Your changes could not be committed, #{base_message}"
+ end
+
+ def merge_error
+ "This merge request cannot be merged, #{base_message}"
+ end
+
+ def push_error
+ "Your push has been rejected, #{base_message}. #{more_info_message}"
+ end
+
+ def new_changes_error
+ "Your push to this repository would cause it to exceed the size limit of #{limit} so it has been rejected. #{more_info_message}"
+ end
+
+ def more_info_message
+ 'Please contact your GitLab administrator for more information.'
+ end
+
+ private
+
+ def base_message
+ "because this repository has exceeded its size limit of #{limit} by #{size_to_remove}"
+ end
+
+ def current_size
+ format_number(project.repository_and_lfs_size)
+ end
+
+ def limit
+ format_number(project.actual_size_limit)
+ end
+
+ def size_to_remove
+ format_number(project.size_to_remove)
+ end
+
+ def format_number(number)
+ number_to_human_size(number * 1.megabyte, delimiter: ',', precision: 2)
+ end
+ end
+end
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 2a89159c0706d272f42ed43f8810f1a5f6f6334f..41d263a46a4d64a5a7d3000e3c7a298c46d46679 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
describe SnippetsController do
- describe 'GET #show' do
- let(:user) { create(:user) }
+ let(:user) { create(:user) }
+ describe 'GET #show' do
context 'when the personal snippet is private' do
let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
@@ -230,4 +230,33 @@
end
end
end
+
+ context 'award emoji on snippets' do
+ let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
+ let(:another_user) { create(:user) }
+
+ before do
+ sign_in(another_user)
+ end
+
+ describe 'POST #toggle_award_emoji' do
+ it "toggles the award emoji" do
+ expect do
+ post(:toggle_award_emoji, id: personal_snippet.to_param, name: "thumbsup")
+ end.to change { personal_snippet.award_emoji.count }.from(0).to(1)
+
+ expect(response.status).to eq(200)
+ end
+
+ it "removes the already awarded emoji" do
+ post(:toggle_award_emoji, id: personal_snippet.to_param, name: "thumbsup")
+
+ expect do
+ post(:toggle_award_emoji, id: personal_snippet.to_param, name: "thumbsup")
+ end.to change { personal_snippet.award_emoji.count }.from(1).to(0)
+
+ expect(response.status).to eq(200)
+ end
+ end
+ end
end
diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb
index 82591604fcb313c77789f7d0117c2f98a1df56c8..6f24bf58d14085b18a4d4b6b50163c60a7fdf4a7 100644
--- a/spec/factories/deployments.rb
+++ b/spec/factories/deployments.rb
@@ -3,11 +3,12 @@
sha '97de212e80737a608d939f648d959671fb0a0142'
ref 'master'
tag false
+ project nil
environment factory: :environment
after(:build) do |deployment, evaluator|
- deployment.project = deployment.environment.project
+ deployment.project ||= deployment.environment.project
end
end
end
diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb
index fc914022a59957d8a415e5dba06454a06bce54e8..3fb1cb37544717b3a1c3747a546c6d056f52979b 100644
--- a/spec/features/dashboard_issues_spec.rb
+++ b/spec/features/dashboard_issues_spec.rb
@@ -21,9 +21,6 @@
click_link 'No Milestone'
- page.within '.issues-state-filters' do
- expect(page).to have_selector('.active .badge', text: '1')
- end
expect(page).to have_selector('.issue', count: 1)
end
@@ -32,9 +29,6 @@
click_link 'Any Milestone'
- page.within '.issues-state-filters' do
- expect(page).to have_selector('.active .badge', text: '2')
- end
expect(page).to have_selector('.issue', count: 2)
end
@@ -45,9 +39,6 @@
click_link milestone.title
end
- page.within '.issues-state-filters' do
- expect(page).to have_selector('.active .badge', text: '1')
- end
expect(page).to have_selector('.issue', count: 1)
end
end
diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb
index 72f39e2fbcaf736766556b95f8cf27656460575d..d1501c9791adc03fa45c75738baa4b3dba7edfb8 100644
--- a/spec/features/issues/filter_issues_spec.rb
+++ b/spec/features/issues/filter_issues_spec.rb
@@ -230,10 +230,6 @@
expect(page).to have_selector('.issue', count: 2)
end
- page.within '.issues-state-filters' do
- expect(page).to have_selector('.active .badge', text: '2')
- end
-
click_button 'Label'
page.within '.labels-filter' do
click_link 'bug'
@@ -243,10 +239,6 @@
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 1)
end
-
- page.within '.issues-state-filters' do
- expect(page).to have_selector('.active .badge', text: '1')
- end
end
it 'filters by text and milestone' do
@@ -256,10 +248,6 @@
expect(page).to have_selector('.issue', count: 2)
end
- page.within '.issues-state-filters' do
- expect(page).to have_selector('.active .badge', text: '2')
- end
-
click_button 'Milestone'
page.within '.milestone-filter' do
click_link '8'
@@ -268,10 +256,6 @@
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 1)
end
-
- page.within '.issues-state-filters' do
- expect(page).to have_selector('.active .badge', text: '1')
- end
end
it 'filters by text and assignee' do
@@ -281,10 +265,6 @@
expect(page).to have_selector('.issue', count: 2)
end
- page.within '.issues-state-filters' do
- expect(page).to have_selector('.active .badge', text: '2')
- end
-
click_button 'Assignee'
page.within '.dropdown-menu-assignee' do
click_link user.name
@@ -293,10 +273,6 @@
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 1)
end
-
- page.within '.issues-state-filters' do
- expect(page).to have_selector('.active .badge', text: '1')
- end
end
it 'filters by text and author' do
@@ -306,10 +282,6 @@
expect(page).to have_selector('.issue', count: 2)
end
- page.within '.issues-state-filters' do
- expect(page).to have_selector('.active .badge', text: '2')
- end
-
click_button 'Author'
page.within '.dropdown-menu-author' do
click_link user.name
@@ -318,10 +290,6 @@
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 1)
end
-
- page.within '.issues-state-filters' do
- expect(page).to have_selector('.active .badge', text: '1')
- end
end
end
end
diff --git a/spec/features/projects/gfm_autocomplete_load_spec.rb b/spec/features/projects/gfm_autocomplete_load_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1921ea6d8aec39c9f0801c6568777ae5fed4a466
--- /dev/null
+++ b/spec/features/projects/gfm_autocomplete_load_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe 'GFM autocomplete loading', feature: true, js: true do
+ let(:project) { create(:project) }
+
+ before do
+ login_as :admin
+
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ it 'does not load on project#show' do
+ expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).to eq('')
+ end
+
+ it 'loads on new issue page' do
+ visit new_namespace_project_issue_path(project.namespace, project)
+
+ expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).not_to eq('')
+ end
+end
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..27c986c5187e0f4f176bfb7baa609d11cfc984ee
--- /dev/null
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+
+# Integration test that exports a file using the Import/Export feature
+# It looks up for any sensitive word inside the JSON, so if a sensitive word is found
+# we''l have to either include it adding the model that includes it to the +safe_list+
+# or make sure the attribute is blacklisted in the +import_export.yml+ configuration
+feature 'Import/Export - project export integration test', feature: true, js: true do
+ include Select2Helper
+ include ExportFileHelper
+
+ let(:user) { create(:admin) }
+ let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
+ let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
+
+ let(:sensitive_words) { %w[pass secret token key] }
+ let(:safe_list) do
+ {
+ token: [ProjectHook, Ci::Trigger, CommitStatus],
+ key: [Project, Ci::Variable, :yaml_variables]
+ }
+ end
+ let(:safe_hashes) { { yaml_variables: %w[key value public] } }
+
+ let(:project) { setup_project }
+
+ background do
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ end
+
+ after do
+ FileUtils.rm_rf(export_path, secure: true)
+ end
+
+ context 'admin user' do
+ before do
+ login_as(user)
+ end
+
+ scenario 'exports a project successfully' do
+ visit edit_namespace_project_path(project.namespace, project)
+
+ expect(page).to have_content('Export project')
+
+ click_link 'Export project'
+
+ visit edit_namespace_project_path(project.namespace, project)
+
+ expect(page).to have_content('Download export')
+
+ in_directory_with_expanded_export(project) do |exit_status, tmpdir|
+ expect(exit_status).to eq(0)
+
+ project_json_path = File.join(tmpdir, 'project.json')
+ expect(File).to exist(project_json_path)
+
+ project_hash = JSON.parse(IO.read(project_json_path))
+
+ sensitive_words.each do |sensitive_word|
+ found = find_sensitive_attributes(sensitive_word, project_hash)
+
+ expect(found).to be_nil, failure_message(found.try(:key_found), found.try(:parent), sensitive_word)
+ end
+ end
+ end
+
+ def failure_message(key_found, parent, sensitive_word)
+ <<-MSG
+ Found a new sensitive word <#{key_found}>, which is part of the hash #{parent.inspect}
+
+ If you think this information shouldn't get exported, please exclude the model or attribute in IMPORT_EXPORT_CONFIG.
+
+ Otherwise, please add the exception to +safe_list+ in CURRENT_SPEC using #{sensitive_word} as the key and the
+ correspondent hash or model as the value.
+
+ IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file}
+ CURRENT_SPEC: #{__FILE__}
+ MSG
+ end
+ end
+end
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index f707ccf4e93e0113d153bbcf655db346a34f67f5..09cd6369881fdaa39613f0d97dec33a786fec1d7 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'project import', feature: true, js: true do
+feature 'Import/Export - project import integration test', feature: true, js: true do
include Select2Helper
let(:admin) { create(:admin) }
diff --git a/spec/lib/ci/mask_secret_spec.rb b/spec/lib/ci/mask_secret_spec.rb
index 518de76911c6716e88e3f248907f34555f0f372d..3101bed20fbab9193efd65e075ab69125342317a 100644
--- a/spec/lib/ci/mask_secret_spec.rb
+++ b/spec/lib/ci/mask_secret_spec.rb
@@ -5,15 +5,23 @@
describe '#mask' do
it 'masks exact number of characters' do
- expect(subject.mask('token', 'oke')).to eq('txxxn')
+ expect(mask('token', 'oke')).to eq('txxxn')
end
it 'masks multiple occurrences' do
- expect(subject.mask('token token token', 'oke')).to eq('txxxn txxxn txxxn')
+ expect(mask('token token token', 'oke')).to eq('txxxn txxxn txxxn')
end
it 'does not mask if not found' do
- expect(subject.mask('token', 'not')).to eq('token')
+ expect(mask('token', 'not')).to eq('token')
+ end
+
+ it 'does support null token' do
+ expect(mask('token', nil)).to eq('token')
+ end
+
+ def mask(value, token)
+ subject.mask!(value.dup, token)
end
end
end
diff --git a/spec/lib/ee/gitlab/ldap/adapter_spec.rb b/spec/lib/ee/gitlab/ldap/adapter_spec.rb
index 5296cb28fa25f88155ea366a76dcc1fe3170908e..e8ce58e419f1091609bd4361ab14fb75138dd20a 100644
--- a/spec/lib/ee/gitlab/ldap/adapter_spec.rb
+++ b/spec/lib/ee/gitlab/ldap/adapter_spec.rb
@@ -7,9 +7,9 @@
expect(Gitlab::LDAP::Adapter).to include_module(EE::Gitlab::LDAP::Adapter)
end
- describe '#groups' do
- let(:adapter) { ldap_adapter('ldapmain') }
+ let(:adapter) { ldap_adapter('ldapmain') }
+ describe '#groups' do
before do
stub_ldap_config(
group_base: 'ou=groups,dc=example,dc=com',
@@ -39,4 +39,11 @@
expect(results.first.member_dns).to match_array(%w(john mary))
end
end
+
+ describe '#user_attributes' do
+ it 'appends EE-specific attributes' do
+ stub_ldap_config(uid: 'uid', sync_ssh_keys: 'sshPublicKey')
+ expect(adapter.user_attributes).to match_array(%w(uid dn cn mail sshPublicKey))
+ end
+ end
end
diff --git a/spec/lib/ee/gitlab/ldap/group_spec.rb b/spec/lib/ee/gitlab/ldap/group_spec.rb
index 3ea5cb87f7fbf08eee541ddc42883ecb0c933fdc..054db487245b7b80e840bf250035368750458f0f 100644
--- a/spec/lib/ee/gitlab/ldap/group_spec.rb
+++ b/spec/lib/ee/gitlab/ldap/group_spec.rb
@@ -3,47 +3,125 @@
describe EE::Gitlab::LDAP::Group, lib: true do
include LdapHelpers
- let(:adapter) { ldap_adapter }
+ before do
+ stub_ldap_config(active_directory: true)
+ end
describe '#member_dns' do
- def ldif
- Net::LDAP::Entry.from_single_ldif_string(
- <<-EOS.strip_heredoc
- dn: cn=ldap_group1,ou=groups,dc=example,dc=com
- cn: ldap_group1
- description: LDAP Group 1
- member: uid=user1,ou=users,dc=example,dc=com
- member: uid=user2,ou=users,dc=example,dc=com
- member: uid=user3,ou=users,dc=example,dc=com
- objectclass: top
- objectclass: groupOfNames
- EOS
+ let(:adapter) { ldap_adapter }
+
+ it 'resolves the correct member_dns when member has a range' do
+ group_entry_page1 = ldap_group_entry_with_member_range(
+ [user_dn('user1'), user_dn('user2'), user_dn('user3')],
+ range_start: '0',
+ range_end: '2'
)
- end
+ group_entry_page2 = ldap_group_entry_with_member_range(
+ [user_dn('user4'), user_dn('user5'), user_dn('user6')],
+ range_start: '3',
+ range_end: '*'
+ )
+ group = EE::Gitlab::LDAP::Group.new(group_entry_page1, adapter)
+ stub_ldap_adapter_group_members_in_range(group_entry_page2, adapter, range_start: '3')
+ stub_ldap_adapter_nested_groups(group.dn, [], adapter)
- let(:group) { described_class.new(ldif, adapter) }
- let(:recursive_dns) do
- %w(
- uid=user3,ou=users,dc=example,dc=com
- uid=user4,ou=users,dc=example,dc=com
- uid=user5,ou=users,dc=example,dc=com
+ expect(group.member_dns).to match_array(
+ %w(
+ uid=user1,ou=users,dc=example,dc=com
+ uid=user2,ou=users,dc=example,dc=com
+ uid=user3,ou=users,dc=example,dc=com
+ uid=user4,ou=users,dc=example,dc=com
+ uid=user5,ou=users,dc=example,dc=com
+ uid=user6,ou=users,dc=example,dc=com
+ )
)
end
- it 'concatenates recursive and regular results and returns uniq' do
- allow(group).to receive(:active_directory?).and_return(true)
- allow(adapter).to receive(:dns_for_filter).and_return(recursive_dns)
+ context 'when there are nested groups' do
+ let(:group1_entry) do
+ ldap_group_entry(
+ [user_dn('user1'), user_dn('user2')],
+ objectclass: 'group',
+ member_attr: 'member'
+ )
+ end
+ let(:group2_entry) do
+ ldap_group_entry(
+ [user_dn('user3'), user_dn('user4')],
+ cn: 'ldap_group2',
+ objectclass: 'group',
+ member_attr: 'member',
+ member_of: group1_entry.dn
+ )
+ end
+ let(:group) { EE::Gitlab::LDAP::Group.new(group1_entry, adapter) }
+
+ it 'resolves the correct member_dns when there are nested groups' do
+ group3_entry = ldap_group_entry(
+ [user_dn('user5'), user_dn('user6')],
+ cn: 'ldap_group3',
+ objectclass: 'group',
+ member_attr: 'member',
+ member_of: group1_entry.dn
+ )
+ nested_groups = [group2_entry, group3_entry]
+ stub_ldap_adapter_nested_groups(group.dn, nested_groups, adapter)
+ stub_ldap_adapter_nested_groups(group2_entry.dn, [], adapter)
+ stub_ldap_adapter_nested_groups(group3_entry.dn, [], adapter)
- expect(group.member_dns)
- .to match_array(
+ expect(group.member_dns).to match_array(
%w(
uid=user1,ou=users,dc=example,dc=com
uid=user2,ou=users,dc=example,dc=com
uid=user3,ou=users,dc=example,dc=com
uid=user4,ou=users,dc=example,dc=com
uid=user5,ou=users,dc=example,dc=com
+ uid=user6,ou=users,dc=example,dc=com
)
)
+ end
+
+ it 'skips duplicate nested groups' do
+ group3_entry = ldap_group_entry(
+ [user_dn('user5'), user_dn('user6')],
+ cn: 'ldap_group3',
+ objectclass: 'group',
+ member_attr: 'member',
+ member_of: [group1_entry.dn, group2_entry.dn]
+ )
+ nested_groups = [group2_entry, group3_entry]
+ stub_ldap_adapter_nested_groups(group.dn, nested_groups, adapter)
+ stub_ldap_adapter_nested_groups(group2_entry.dn, [group3_entry], adapter)
+ stub_ldap_adapter_nested_groups(group3_entry.dn, [], adapter)
+
+ expect(adapter).to receive(:nested_groups).with(group3_entry.dn).once
+
+ group.member_dns
+ end
+
+ it 'does not include group dns or users outside of the base' do
+ # Spaces in the 3rd DN below are intentional to ensure we're sanitizing
+ # DNs before comparing and not just doing a string compare.
+ group3_entry = ldap_group_entry(
+ [
+ 'cn=ldap_group2,ou=groups,dc=example,dc=com',
+ 'uid=foo,ou=users,dc=other,dc=com',
+ 'uid=bar,ou=users,dc=example , dc=com'
+ ],
+ cn: 'ldap_group3',
+ objectclass: 'group',
+ member_attr: 'member',
+ member_of: group1_entry.dn
+ )
+ nested_groups = [group2_entry, group3_entry]
+ stub_ldap_adapter_nested_groups(group.dn, nested_groups, adapter)
+ stub_ldap_adapter_nested_groups(group2_entry.dn, [], adapter)
+ stub_ldap_adapter_nested_groups(group3_entry.dn, [], adapter)
+
+ expect(group.member_dns).not_to include('cn=ldap_group1,ou=groups,dc=example,dc=com')
+ expect(group.member_dns).not_to include('uid=foo,ou=users,dc=other,dc=com')
+ expect(group.member_dns).to include('uid=bar,ou=users,dc=example , dc=com')
+ end
end
end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 8674b70cc551945674c929c373b92d07686fa456..04ed0023aa5aa7ee4592383fd1f38a5709171526 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -618,6 +618,24 @@ def self.run_permission_checks(permissions_matrix)
expect(access.push_access_check('cfe32cf61b73a0d5e9f13e774abde7ff789b1660 913c66a37b4a45b9769037c55c2d238bd0942d2e refs/heads/master')).to be_allowed
end
end
+
+ describe 'repository size restrictions' do
+ before do
+ project.update_attribute(:repository_size_limit, 50)
+ end
+
+ it 'returns false when blob is too big' do
+ allow_any_instance_of(Gitlab::Git::Blob).to receive(:size).and_return(100.megabytes.to_i)
+
+ expect(access.push_access_check('cfe32cf61b73a0d5e9f13e774abde7ff789b1660 913c66a37b4a45b9769037c55c2d238bd0942d2e refs/heads/master')).not_to be_allowed
+ end
+
+ it 'returns true when blob is just right' do
+ allow_any_instance_of(Gitlab::Git::Blob).to receive(:size).and_return(2.megabytes.to_i)
+
+ expect(access.push_access_check('cfe32cf61b73a0d5e9f13e774abde7ff789b1660 913c66a37b4a45b9769037c55c2d238bd0942d2e refs/heads/master')).to be_allowed
+ end
+ end
end
end
@@ -644,7 +662,7 @@ def self.run_permission_checks(permissions_matrix)
end
context 'to private project' do
- let(:project) { create(:project, :internal) }
+ let(:project) { create(:project) }
it { expect(subject).not_to be_allowed }
end
diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..219198eff60f5d203ff0c89ff6246b861f407c8f
--- /dev/null
+++ b/spec/lib/gitlab/git_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Gitlab::Git, lib: true do
+ let(:committer_email) { FFaker::Internet.email }
+
+ # I have to remove periods from the end of the name
+ # This happened when the user's name had a suffix (i.e. "Sr.")
+ # This seems to be what git does under the hood. For example, this commit:
+ #
+ # $ git commit --author='Foo Sr. ' -m 'Where's my trailing period?'
+ #
+ # results in this:
+ #
+ # $ git show --pretty
+ # ...
+ # Author: Foo Sr
+ # ...
+ let(:committer_name) { FFaker::Name.name.chomp("\.") }
+
+ describe 'committer_hash' do
+ it "returns a hash containing the given email and name" do
+ committer_hash = Gitlab::Git::committer_hash(email: committer_email, name: committer_name)
+
+ expect(committer_hash[:email]).to eq(committer_email)
+ expect(committer_hash[:name]).to eq(committer_name)
+ expect(committer_hash[:time]).to be_a(Time)
+ end
+
+ context 'when email is nil' do
+ it "returns nil" do
+ committer_hash = Gitlab::Git::committer_hash(email: nil, name: committer_name)
+
+ expect(committer_hash).to be_nil
+ end
+ end
+
+ context 'when name is nil' do
+ it "returns nil" do
+ committer_hash = Gitlab::Git::committer_hash(email: committer_email, name: nil)
+
+ expect(committer_hash).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
new file mode 100644
index 0000000000000000000000000000000000000000..55f37f5ab0d458a9ede33a9624177270921069fa
--- /dev/null
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -0,0 +1,201 @@
+---
+issues:
+- subscriptions
+- award_emoji
+- author
+- assignee
+- updated_by
+- milestone
+- notes
+- label_links
+- labels
+- todos
+- user_agent_detail
+- moved_to
+- events
+- merge_requests_closing_issues
+- metrics
+events:
+- author
+- project
+- target
+notes:
+- award_emoji
+- project
+- noteable
+- author
+- updated_by
+- resolved_by
+- todos
+- events
+label_links:
+- target
+- label
+label:
+- subscriptions
+- project
+- lists
+- label_links
+- issues
+- merge_requests
+milestone:
+- project
+- issues
+- labels
+- merge_requests
+- participants
+- events
+snippets:
+- author
+- project
+- notes
+- award_emoji
+releases:
+- project
+project_members:
+- created_by
+- user
+- source
+- project
+merge_requests:
+- subscriptions
+- award_emoji
+- author
+- assignee
+- updated_by
+- milestone
+- notes
+- label_links
+- labels
+- todos
+- target_project
+- source_project
+- merge_user
+- merge_request_diffs
+- merge_request_diff
+- events
+- merge_requests_closing_issues
+- metrics
+- approvals
+- approvers
+merge_request_diff:
+- merge_request
+pipelines:
+- project
+- user
+- statuses
+- builds
+- trigger_requests
+statuses:
+- project
+- pipeline
+- user
+variables:
+- project
+triggers:
+- project
+- trigger_requests
+deploy_keys:
+- user
+- deploy_keys_projects
+- projects
+services:
+- project
+- service_hook
+hooks:
+- project
+protected_branches:
+- project
+- merge_access_levels
+- push_access_levels
+merge_access_levels:
+- user
+- protected_branch
+push_access_levels:
+- user
+- protected_branch
+project:
+- taggings
+- base_tags
+- tag_taggings
+- tags
+- creator
+- group
+- namespace
+- board
+- last_event
+- services
+- campfire_service
+- drone_ci_service
+- emails_on_push_service
+- builds_email_service
+- irker_service
+- pivotaltracker_service
+- hipchat_service
+- flowdock_service
+- assembla_service
+- asana_service
+- gemnasium_service
+- slack_service
+- buildkite_service
+- bamboo_service
+- teamcity_service
+- pushover_service
+- jira_service
+- redmine_service
+- custom_issue_tracker_service
+- bugzilla_service
+- gitlab_issue_tracker_service
+- external_wiki_service
+- forked_project_link
+- forked_from_project
+- forked_project_links
+- forks
+- merge_requests
+- fork_merge_requests
+- issues
+- labels
+- events
+- milestones
+- notes
+- snippets
+- hooks
+- protected_branches
+- project_members
+- users
+- requesters
+- deploy_keys_projects
+- deploy_keys
+- users_star_projects
+- starrers
+- releases
+- lfs_objects_projects
+- lfs_objects
+- project_group_links
+- invited_groups
+- todos
+- notification_settings
+- import_data
+- commit_statuses
+- pipelines
+- builds
+- runner_projects
+- runners
+- variables
+- triggers
+- environments
+- deployments
+- project_feature
+- mirror_user
+- push_rule
+- jenkins_service
+- jenkins_deprecated_service
+- index_status
+- approvers
+- pages_domains
+- audit_events
+- remote_mirrors
+- path_locks
+award_emoji:
+- awardable
+- user
diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2ba344092cefc411c26a8da093cd34a071adf28d
--- /dev/null
+++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+# Part of the test security suite for the Import/Export feature
+# Checks whether there are new attributes in models that are currently being exported as part of the
+# project Import/Export feature.
+# If there are new attributes, these will have to either be added to this spec in case we want them
+# to be included as part of the export, or blacklist them using the import_export.yml configuration file.
+# Likewise, new models added to import_export.yml, will need to be added with their correspondent attributes
+# to this spec.
+describe 'Import/Export attribute configuration', lib: true do
+ include ConfigurationHelper
+
+ let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
+ let(:relation_names) do
+ names = names_from_tree(config_hash['project_tree'])
+
+ # Remove duplicated or add missing models
+ # - project is not part of the tree, so it has to be added manually.
+ # - milestone, labels have both singular and plural versions in the tree, so remove the duplicates.
+ names.flatten.uniq - ['milestones', 'labels'] + ['project']
+ end
+
+ let(:safe_attributes_file) { 'spec/lib/gitlab/import_export/safe_model_attributes.yml' }
+ let(:safe_model_attributes) { YAML.load_file(safe_attributes_file) }
+
+ it 'has no new columns' do
+ relation_names.each do |relation_name|
+ relation_class = relation_class_for_name(relation_name)
+
+ expect(safe_model_attributes[relation_class.to_s]).not_to be_nil, "Expected exported class #{relation_class.to_s} to exist in safe_model_attributes"
+
+ current_attributes = parsed_attributes(relation_name, relation_class.attribute_names)
+ safe_attributes = safe_model_attributes[relation_class.to_s]
+ new_attributes = current_attributes - safe_attributes
+
+ expect(new_attributes).to be_empty, failure_message(relation_class.to_s, new_attributes)
+ end
+ end
+
+ def failure_message(relation_class, new_attributes)
+ <<-MSG
+ It looks like #{relation_class}, which is exported using the project Import/Export, has new attributes: #{new_attributes.join(',')}
+
+ Please add the attribute(s) to SAFE_MODEL_ATTRIBUTES if you consider this can be exported.
+ Otherwise, please blacklist the attribute(s) in IMPORT_EXPORT_CONFIG by adding it to its correspondent
+ model in the +excluded_attributes+ section.
+
+ SAFE_MODEL_ATTRIBUTES: #{File.expand_path(safe_attributes_file)}
+ IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file}
+ MSG
+ end
+
+ class Author < User
+ end
+end
diff --git a/spec/lib/gitlab/import_export/model_configuration_spec.rb b/spec/lib/gitlab/import_export/model_configuration_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9b492d1b9c7b4a0e24c73ed84639b5d151588154
--- /dev/null
+++ b/spec/lib/gitlab/import_export/model_configuration_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+# Part of the test security suite for the Import/Export feature
+# Finds if a new model has been added that can potentially be part of the Import/Export
+# If it finds a new model, it will show a +failure_message+ with the options available.
+describe 'Import/Export model configuration', lib: true do
+ include ConfigurationHelper
+
+ let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
+ let(:model_names) do
+ names = names_from_tree(config_hash['project_tree'])
+
+ # Remove duplicated or add missing models
+ # - project is not part of the tree, so it has to be added manually.
+ # - milestone, labels have both singular and plural versions in the tree, so remove the duplicates.
+ # - User, Author... Models we do not care about for checking models
+ names.flatten.uniq - ['milestones', 'labels', 'user', 'author'] + ['project']
+ end
+
+ let(:all_models_yml) { 'spec/lib/gitlab/import_export/all_models.yml' }
+ let(:all_models) { YAML.load_file(all_models_yml) }
+ let(:current_models) { setup_models }
+
+ it 'has no new models' do
+ model_names.each do |model_name|
+ new_models = Array(current_models[model_name]) - Array(all_models[model_name])
+ expect(new_models).to be_empty, failure_message(model_name.classify, new_models)
+ end
+ end
+
+ # List of current models between models, in the format of
+ # {model: [model_2, model3], ...}
+ def setup_models
+ all_models_hash = {}
+
+ model_names.each do |model_name|
+ model_class = relation_class_for_name(model_name)
+
+ all_models_hash[model_name] = associations_for(model_class) - ['project']
+ end
+
+ all_models_hash
+ end
+
+ def failure_message(parent_model_name, new_models)
+ <<-MSG
+ New model(s) <#{new_models.join(',')}> have been added, related to #{parent_model_name}, which is exported by
+ the Import/Export feature.
+
+ If you think this model should be included in the export, please add it to IMPORT_EXPORT_CONFIG.
+ Definitely add it to MODELS_JSON to signal that you've handled this error and to prevent it from showing up in the future.
+
+ MODELS_JSON: #{File.expand_path(all_models_yml)}
+ IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file}
+ MSG
+ end
+end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e0910f4ba6dcd6132d28b051fe9615a396eb405c
--- /dev/null
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -0,0 +1,334 @@
+---
+Issue:
+- id
+- title
+- assignee_id
+- author_id
+- project_id
+- created_at
+- updated_at
+- position
+- branch_name
+- description
+- state
+- iid
+- updated_by_id
+- confidential
+- deleted_at
+- due_date
+- moved_to_id
+- lock_version
+- milestone_id
+- weight
+Event:
+- id
+- target_type
+- target_id
+- title
+- data
+- project_id
+- created_at
+- updated_at
+- action
+- author_id
+Note:
+- id
+- note
+- noteable_type
+- author_id
+- created_at
+- updated_at
+- project_id
+- attachment
+- line_code
+- commit_id
+- noteable_id
+- system
+- st_diff
+- updated_by_id
+- type
+- position
+- original_position
+- resolved_at
+- resolved_by_id
+- discussion_id
+- original_discussion_id
+LabelLink:
+- id
+- label_id
+- target_id
+- target_type
+- created_at
+- updated_at
+Label:
+- id
+- title
+- color
+- project_id
+- created_at
+- updated_at
+- template
+- description
+- priority
+Milestone:
+- id
+- title
+- project_id
+- description
+- due_date
+- created_at
+- updated_at
+- state
+- iid
+ProjectSnippet:
+- id
+- title
+- content
+- author_id
+- project_id
+- created_at
+- updated_at
+- file_name
+- type
+- visibility_level
+Release:
+- id
+- tag
+- description
+- project_id
+- created_at
+- updated_at
+ProjectMember:
+- id
+- access_level
+- source_id
+- source_type
+- user_id
+- notification_level
+- type
+- created_at
+- updated_at
+- created_by_id
+- invite_email
+- invite_token
+- invite_accepted_at
+- requested_at
+- expires_at
+- ldap
+- override
+User:
+- id
+- username
+- email
+MergeRequest:
+- id
+- target_branch
+- source_branch
+- source_project_id
+- author_id
+- assignee_id
+- title
+- created_at
+- updated_at
+- state
+- merge_status
+- target_project_id
+- iid
+- description
+- position
+- locked_at
+- updated_by_id
+- merge_error
+- merge_params
+- merge_when_build_succeeds
+- merge_user_id
+- merge_commit_sha
+- deleted_at
+- in_progress_merge_commit_sha
+- lock_version
+- milestone_id
+- approvals_before_merge
+- rebase_commit_sha
+MergeRequestDiff:
+- id
+- state
+- st_commits
+- merge_request_id
+- created_at
+- updated_at
+- base_commit_sha
+- real_size
+- head_commit_sha
+- start_commit_sha
+Ci::Pipeline:
+- id
+- project_id
+- ref
+- sha
+- before_sha
+- push_data
+- created_at
+- updated_at
+- tag
+- yaml_errors
+- committed_at
+- gl_project_id
+- status
+- started_at
+- finished_at
+- duration
+- user_id
+CommitStatus:
+- id
+- project_id
+- status
+- finished_at
+- trace
+- created_at
+- updated_at
+- started_at
+- runner_id
+- coverage
+- commit_id
+- commands
+- job_id
+- name
+- deploy
+- options
+- allow_failure
+- stage
+- trigger_request_id
+- stage_idx
+- tag
+- ref
+- user_id
+- type
+- target_url
+- description
+- artifacts_file
+- gl_project_id
+- artifacts_metadata
+- erased_by_id
+- erased_at
+- artifacts_expire_at
+- environment
+- artifacts_size
+- when
+- yaml_variables
+- queued_at
+- token
+Ci::Variable:
+- id
+- project_id
+- key
+- value
+- encrypted_value
+- encrypted_value_salt
+- encrypted_value_iv
+- gl_project_id
+Ci::Trigger:
+- id
+- token
+- project_id
+- deleted_at
+- created_at
+- updated_at
+- gl_project_id
+DeployKey:
+- id
+- user_id
+- created_at
+- updated_at
+- key
+- title
+- type
+- fingerprint
+- public
+Service:
+- id
+- type
+- title
+- project_id
+- created_at
+- updated_at
+- active
+- properties
+- template
+- push_events
+- issues_events
+- merge_requests_events
+- tag_push_events
+- note_events
+- pipeline_events
+- build_events
+- category
+- default
+- wiki_page_events
+- confidential_issues_events
+ProjectHook:
+- id
+- url
+- project_id
+- created_at
+- updated_at
+- type
+- service_id
+- push_events
+- issues_events
+- merge_requests_events
+- tag_push_events
+- note_events
+- pipeline_events
+- enable_ssl_verification
+- build_events
+- wiki_page_events
+- token
+- group_id
+- confidential_issues_events
+ProtectedBranch:
+- id
+- project_id
+- name
+- created_at
+- updated_at
+Project:
+- description
+- issues_enabled
+- merge_requests_enabled
+- wiki_enabled
+- snippets_enabled
+- visibility_level
+- archived
+Author:
+- name
+ProjectFeature:
+- id
+- project_id
+- merge_requests_access_level
+- issues_access_level
+- wiki_access_level
+- snippets_access_level
+- builds_access_level
+- created_at
+- updated_at
+ProtectedBranch::MergeAccessLevel:
+- id
+- protected_branch_id
+- access_level
+- created_at
+- updated_at
+- user_id
+ProtectedBranch::PushAccessLevel:
+- id
+- protected_branch_id
+- access_level
+- created_at
+- updated_at
+- user_id
+AwardEmoji:
+- id
+- user_id
+- name
+- awardable_type
+- created_at
+- updated_at
diff --git a/spec/lib/gitlab/repository_size_error_spec.rb b/spec/lib/gitlab/repository_size_error_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..473f2abf9e0cd5fd766d902b9a088b63000142f4
--- /dev/null
+++ b/spec/lib/gitlab/repository_size_error_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Gitlab::RepositorySizeError, lib: true do
+ let(:project) { create(:empty_project, repository_size: 15) }
+ let(:message) { Gitlab::RepositorySizeError.new(project) }
+ let(:base_message) { 'because this repository has exceeded its size limit of 10 MB by 5 MB' }
+
+ before do
+ allow(project).to receive(:actual_size_limit).and_return(10)
+ end
+
+ describe 'error messages' do
+ describe '#to_s' do
+ it 'returns the correct message' do
+ expect(message.to_s).to eq('The size of this repository (15 MB) exceeds the limit of 10 MB by 5 MB.')
+ end
+ end
+
+ describe '#commit_error' do
+ it 'returns the correct message' do
+ expect(message.commit_error).to eq("Your changes could not be committed, #{base_message}")
+ end
+ end
+
+ describe '#merge_error' do
+ it 'returns the correct message' do
+ expect(message.merge_error).to eq("This merge request cannot be merged, #{base_message}")
+ end
+ end
+
+ describe '#push_error' do
+ it 'returns the correct message' do
+ expect(message.push_error).to eq("Your push has been rejected, #{base_message}. #{message.more_info_message}")
+ end
+ end
+
+ describe '#new_changes_error' do
+ it 'returns the correct message' do
+ expect(message.new_changes_error).to eq("Your push to this repository would cause it to exceed the size limit of 10 MB so it has been rejected. #{message.more_info_message}")
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index f1857f846dcf6a3aa3608016812a6da905d67520..550a890797e5bda8dba9723ee0f5a3b936bdaf3a 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -187,6 +187,37 @@
end
end
+ describe "merge request metrics" do
+ let(:project) { FactoryGirl.create :project }
+ let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) }
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) }
+
+ context 'when transitioning to running' do
+ it 'records the build start time' do
+ time = Time.now
+ Timecop.freeze(time) { build.run }
+
+ expect(merge_request.reload.metrics.latest_build_started_at).to be_within(1.second).of(time)
+ end
+
+ it 'clears the build end time' do
+ build.run
+
+ expect(merge_request.reload.metrics.latest_build_finished_at).to be_nil
+ end
+ end
+
+ context 'when transitioning to success' do
+ it 'records the build end time' do
+ build.run
+ time = Time.now
+ Timecop.freeze(time) { build.success }
+
+ expect(merge_request.reload.metrics.latest_build_finished_at).to be_within(1.second).of(time)
+ end
+ end
+ end
+
def create_build(name, queued_at = current, started_from = 0)
create(:ci_build,
name: name,
@@ -468,4 +499,28 @@ def create_build(name, stage_idx)
stage_idx: stage_idx)
end
end
+
+ describe "#merge_requests" do
+ let(:project) { FactoryGirl.create :project }
+ let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) }
+
+ it "returns merge requests whose `diff_head_sha` matches the pipeline's SHA" do
+ merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref)
+
+ expect(pipeline.merge_requests).to eq([merge_request])
+ end
+
+ it "doesn't return merge requests whose source branch doesn't match the pipeline's ref" do
+ create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master')
+
+ expect(pipeline.merge_requests).to be_empty
+ end
+
+ it "doesn't return merge requests whose `diff_head_sha` doesn't match the pipeline's SHA" do
+ create(:merge_request, source_project: project, source_branch: pipeline.ref)
+ allow_any_instance_of(MergeRequest).to receive(:diff_head_sha) { '97de212e80737a608d939f648d959671fb0a0142b' }
+
+ expect(pipeline.merge_requests).to be_empty
+ end
+ end
end
diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b9381e3391411e0da08bff5ca8ee16a4748a32ad
--- /dev/null
+++ b/spec/models/cycle_analytics/code_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#code', feature: true do
+ extend CycleAnalyticsHelpers::TestGeneration
+
+ let(:project) { create(:project) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ subject { CycleAnalytics.new(project, from: from_date) }
+
+ generate_cycle_analytics_spec(
+ phase: :code,
+ data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } },
+ start_time_conditions: [["issue mentioned in a commit",
+ -> (context, data) do
+ context.create_commit_referencing_issue(data[:issue])
+ end]],
+ end_time_conditions: [["merge request that closes issue is created",
+ -> (context, data) do
+ context.create_merge_request_closing_issue(data[:issue])
+ end]],
+ post_fn: -> (context, data) do
+ context.merge_merge_requests_closing_issue(data[:issue])
+ context.deploy_master
+ end)
+
+ context "when a regular merge request (that doesn't close the issue) is created" do
+ it "returns nil" do
+ 5.times do
+ issue = create(:issue, project: project)
+
+ create_commit_referencing_issue(issue)
+ create_merge_request_closing_issue(issue, message: "Closes nothing")
+
+ merge_merge_requests_closing_issue(issue)
+ deploy_master
+ end
+
+ expect(subject.code).to be_nil
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e9cc71254ab46619a65fe80ce7c79793f107fc05
--- /dev/null
+++ b/spec/models/cycle_analytics/issue_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#issue', models: true do
+ extend CycleAnalyticsHelpers::TestGeneration
+
+ let(:project) { create(:project) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ subject { CycleAnalytics.new(project, from: from_date) }
+
+ generate_cycle_analytics_spec(
+ phase: :issue,
+ data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } },
+ start_time_conditions: [["issue created", -> (context, data) { data[:issue].save }]],
+ end_time_conditions: [["issue associated with a milestone",
+ -> (context, data) do
+ if data[:issue].persisted?
+ data[:issue].update(milestone: context.create(:milestone, project: context.project))
+ end
+ end],
+ ["list label added to issue",
+ -> (context, data) do
+ if data[:issue].persisted?
+ data[:issue].update(label_ids: [context.create(:label, lists: [context.create(:list)]).id])
+ end
+ end]],
+ post_fn: -> (context, data) do
+ if data[:issue].persisted?
+ context.create_merge_request_closing_issue(data[:issue].reload)
+ context.merge_merge_requests_closing_issue(data[:issue])
+ context.deploy_master
+ end
+ end)
+
+ context "when a regular label (instead of a list label) is added to the issue" do
+ it "returns nil" do
+ 5.times do
+ regular_label = create(:label)
+ issue = create(:issue, project: project)
+ issue.update(label_ids: [regular_label.id])
+
+ create_merge_request_closing_issue(issue)
+ merge_merge_requests_closing_issue(issue)
+ deploy_master
+ end
+
+ expect(subject.issue).to be_nil
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5b8c96dc992189169204304b0861bd6bee980371
--- /dev/null
+++ b/spec/models/cycle_analytics/plan_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#plan', feature: true do
+ extend CycleAnalyticsHelpers::TestGeneration
+
+ let(:project) { create(:project) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ subject { CycleAnalytics.new(project, from: from_date) }
+
+ generate_cycle_analytics_spec(
+ phase: :plan,
+ data_fn: -> (context) do
+ {
+ issue: context.create(:issue, project: context.project),
+ branch_name: context.random_git_name
+ }
+ end,
+ start_time_conditions: [["issue associated with a milestone",
+ -> (context, data) do
+ data[:issue].update(milestone: context.create(:milestone, project: context.project))
+ end],
+ ["list label added to issue",
+ -> (context, data) do
+ data[:issue].update(label_ids: [context.create(:label, lists: [context.create(:list)]).id])
+ end]],
+ end_time_conditions: [["issue mentioned in a commit",
+ -> (context, data) do
+ context.create_commit_referencing_issue(data[:issue], branch_name: data[:branch_name])
+ end]],
+ post_fn: -> (context, data) do
+ context.create_merge_request_closing_issue(data[:issue], source_branch: data[:branch_name])
+ context.merge_merge_requests_closing_issue(data[:issue])
+ context.deploy_master
+ end)
+
+ context "when a regular label (instead of a list label) is added to the issue" do
+ it "returns nil" do
+ branch_name = random_git_name
+ label = create(:label)
+ issue = create(:issue, project: project)
+ issue.update(label_ids: [label.id])
+ create_commit_referencing_issue(issue, branch_name: branch_name)
+
+ create_merge_request_closing_issue(issue, source_branch: branch_name)
+ merge_merge_requests_closing_issue(issue)
+ deploy_master
+
+ expect(subject.issue).to be_nil
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1f5e5cab92d0698f105b721f1f796b4d2af54623
--- /dev/null
+++ b/spec/models/cycle_analytics/production_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#production', feature: true do
+ extend CycleAnalyticsHelpers::TestGeneration
+
+ let(:project) { create(:project) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ subject { CycleAnalytics.new(project, from: from_date) }
+
+ generate_cycle_analytics_spec(
+ phase: :production,
+ data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } },
+ start_time_conditions: [["issue is created", -> (context, data) { data[:issue].save }]],
+ before_end_fn: lambda do |context, data|
+ context.create_merge_request_closing_issue(data[:issue])
+ context.merge_merge_requests_closing_issue(data[:issue])
+ end,
+ end_time_conditions:
+ [["merge request that closes issue is deployed to production", -> (context, data) { context.deploy_master }],
+ ["production deploy happens after merge request is merged (along with other changes)",
+ lambda do |context, data|
+ # Make other changes on master
+ sha = context.project.repository.commit_file(context.user, context.random_git_name, "content", "commit message", 'master', false)
+ context.project.repository.commit(sha)
+
+ context.deploy_master
+ end]])
+
+ context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
+ it "returns nil" do
+ 5.times do
+ merge_request = create(:merge_request)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master
+ end
+
+ expect(subject.production).to be_nil
+ end
+ end
+
+ context "when the deployment happens to a non-production environment" do
+ it "returns nil" do
+ 5.times do
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master(environment: 'staging')
+ end
+
+ expect(subject.production).to be_nil
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b6e26d8f261722ea5af3b23866ff76f630d1e8db
--- /dev/null
+++ b/spec/models/cycle_analytics/review_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#review', feature: true do
+ extend CycleAnalyticsHelpers::TestGeneration
+
+ let(:project) { create(:project) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ subject { CycleAnalytics.new(project, from: from_date) }
+
+ generate_cycle_analytics_spec(
+ phase: :review,
+ data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } },
+ start_time_conditions: [["merge request that closes issue is created",
+ -> (context, data) do
+ context.create_merge_request_closing_issue(data[:issue])
+ end]],
+ end_time_conditions: [["merge request that closes issue is merged",
+ -> (context, data) do
+ context.merge_merge_requests_closing_issue(data[:issue])
+ end]],
+ post_fn: -> (context, data) { context.deploy_master })
+
+ context "when a regular merge request (that doesn't close the issue) is created and merged" do
+ it "returns nil" do
+ 5.times do
+ MergeRequests::MergeService.new(project, user).execute(create(:merge_request))
+
+ deploy_master
+ end
+
+ expect(subject.review).to be_nil
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..af1c4477ddb58bdd9f7ba2dfccdc3961362ccc19
--- /dev/null
+++ b/spec/models/cycle_analytics/staging_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#staging', feature: true do
+ extend CycleAnalyticsHelpers::TestGeneration
+
+ let(:project) { create(:project) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ subject { CycleAnalytics.new(project, from: from_date) }
+
+ generate_cycle_analytics_spec(
+ phase: :staging,
+ data_fn: lambda do |context|
+ issue = context.create(:issue, project: context.project)
+ { issue: issue, merge_request: context.create_merge_request_closing_issue(issue) }
+ end,
+ start_time_conditions: [["merge request that closes issue is merged",
+ -> (context, data) do
+ context.merge_merge_requests_closing_issue(data[:issue])
+ end ]],
+ end_time_conditions: [["merge request that closes issue is deployed to production",
+ -> (context, data) do
+ context.deploy_master
+ end],
+ ["production deploy happens after merge request is merged (along with other changes)",
+ lambda do |context, data|
+ # Make other changes on master
+ sha = context.project.repository.commit_file(
+ context.user,
+ context.random_git_name,
+ "content",
+ "commit message",
+ 'master',
+ false)
+ context.project.repository.commit(sha)
+
+ context.deploy_master
+ end]])
+
+ context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
+ it "returns nil" do
+ 5.times do
+ merge_request = create(:merge_request)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master
+ end
+
+ expect(subject.staging).to be_nil
+ end
+ end
+
+ context "when the deployment happens to a non-production environment" do
+ it "returns nil" do
+ 5.times do
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ MergeRequests::MergeService.new(project, user).execute(merge_request)
+ deploy_master(environment: 'staging')
+ end
+
+ expect(subject.staging).to be_nil
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/summary_spec.rb b/spec/models/cycle_analytics/summary_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..743bc2da33fbeb8dded2aaecf945034b9bb0e007
--- /dev/null
+++ b/spec/models/cycle_analytics/summary_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+describe CycleAnalytics::Summary, models: true do
+ let(:project) { create(:project) }
+ let(:from) { Time.now }
+ let(:user) { create(:user, :admin) }
+ subject { described_class.new(project, from: from) }
+
+ describe "#new_issues" do
+ it "finds the number of issues created after the 'from date'" do
+ Timecop.freeze(5.days.ago) { create(:issue, project: project) }
+ Timecop.freeze(5.days.from_now) { create(:issue, project: project) }
+
+ expect(subject.new_issues).to eq(1)
+ end
+
+ it "doesn't find issues from other projects" do
+ Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project)) }
+
+ expect(subject.new_issues).to eq(0)
+ end
+ end
+
+ describe "#commits" do
+ it "finds the number of commits created after the 'from date'" do
+ Timecop.freeze(5.days.ago) { create_commit("Test message", project, user, 'master') }
+ Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master') }
+
+ expect(subject.commits).to eq(1)
+ end
+
+ it "doesn't find commits from other projects" do
+ Timecop.freeze(5.days.from_now) { create_commit("Test message", create(:project), user, 'master') }
+
+ expect(subject.commits).to eq(0)
+ end
+ end
+
+ describe "#deploys" do
+ it "finds the number of deploys made created after the 'from date'" do
+ Timecop.freeze(5.days.ago) { create(:deployment, project: project) }
+ Timecop.freeze(5.days.from_now) { create(:deployment, project: project) }
+
+ expect(subject.deploys).to eq(1)
+ end
+
+ it "doesn't find commits from other projects" do
+ Timecop.freeze(5.days.from_now) { create(:deployment, project: create(:project)) }
+
+ expect(subject.deploys).to eq(0)
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..89ace0b274278379e3c0ead96136036a968c20ba
--- /dev/null
+++ b/spec/models/cycle_analytics/test_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe 'CycleAnalytics#test', feature: true do
+ extend CycleAnalyticsHelpers::TestGeneration
+
+ let(:project) { create(:project) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ subject { CycleAnalytics.new(project, from: from_date) }
+
+ generate_cycle_analytics_spec(
+ phase: :test,
+ data_fn: lambda do |context|
+ issue = context.create(:issue, project: context.project)
+ merge_request = context.create_merge_request_closing_issue(issue)
+ pipeline = context.create(:ci_pipeline, ref: merge_request.source_branch, sha: merge_request.diff_head_sha, project: context.project)
+ { pipeline: pipeline, issue: issue }
+ end,
+ start_time_conditions: [["pipeline is started", -> (context, data) { data[:pipeline].run! }]],
+ end_time_conditions: [["pipeline is finished", -> (context, data) { data[:pipeline].succeed! }]],
+ post_fn: -> (context, data) do
+ context.merge_merge_requests_closing_issue(data[:issue])
+ context.deploy_master
+ end)
+
+ context "when the pipeline is for a regular merge request (that doesn't close an issue)" do
+ it "returns nil" do
+ 5.times do
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
+
+ pipeline.run!
+ pipeline.succeed!
+
+ merge_merge_requests_closing_issue(issue)
+ deploy_master
+ end
+
+ expect(subject.test).to be_nil
+ end
+ end
+
+ context "when the pipeline is not for a merge request" do
+ it "returns nil" do
+ 5.times do
+ pipeline = create(:ci_pipeline, ref: "refs/heads/master", sha: project.repository.commit('master').sha)
+
+ pipeline.run!
+ pipeline.succeed!
+
+ deploy_master
+ end
+
+ expect(subject.test).to be_nil
+ end
+ end
+
+ context "when the pipeline is dropped (failed)" do
+ it "returns nil" do
+ 5.times do
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
+
+ pipeline.run!
+ pipeline.drop!
+
+ merge_merge_requests_closing_issue(issue)
+ deploy_master
+ end
+
+ expect(subject.test).to be_nil
+ end
+ end
+
+ context "when the pipeline is cancelled" do
+ it "returns nil" do
+ 5.times do
+ issue = create(:issue, project: project)
+ merge_request = create_merge_request_closing_issue(issue)
+ pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
+
+ pipeline.run!
+ pipeline.cancel!
+
+ merge_merge_requests_closing_issue(issue)
+ deploy_master
+ end
+
+ expect(subject.test).to be_nil
+ end
+ end
+end
diff --git a/spec/models/ee/group_spec.rb b/spec/models/ee/group_spec.rb
index f8916d61bf6c3a887ec76fbbfa34ff2d7d54911e..62d3bdbcd89681ed1a0af045c85f56aed783949a 100644
--- a/spec/models/ee/group_spec.rb
+++ b/spec/models/ee/group_spec.rb
@@ -85,4 +85,22 @@
expect { group.mark_ldap_sync_as_failed('Error') }.not_to raise_error
end
end
+
+ describe '#actual_size_limit' do
+ let(:group) { build(:group) }
+
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:repository_size_limit).and_return(50)
+ end
+
+ it 'returns the value set globally' do
+ expect(group.actual_size_limit).to eq(50)
+ end
+
+ it 'returns the value set locally' do
+ group.update_attribute(:repository_size_limit, 75)
+
+ expect(group.actual_size_limit).to eq(75)
+ end
+ end
end
diff --git a/spec/models/issue/metrics_spec.rb b/spec/models/issue/metrics_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e170b087ebcba10a6f51c24718ba229a78ffee37
--- /dev/null
+++ b/spec/models/issue/metrics_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Issue::Metrics, models: true do
+ let(:project) { create(:project) }
+
+ subject { create(:issue, project: project) }
+
+ describe "when recording the default set of issue metrics on issue save" do
+ context "milestones" do
+ it "records the first time an issue is associated with a milestone" do
+ time = Time.now
+ Timecop.freeze(time) { subject.update(milestone: create(:milestone)) }
+ metrics = subject.metrics
+
+ expect(metrics).to be_present
+ expect(metrics.first_associated_with_milestone_at).to be_within(1.second).of(time)
+ end
+
+ it "does not record the second time an issue is associated with a milestone" do
+ time = Time.now
+ Timecop.freeze(time) { subject.update(milestone: create(:milestone)) }
+ Timecop.freeze(time + 2.hours) { subject.update(milestone: nil) }
+ Timecop.freeze(time + 6.hours) { subject.update(milestone: create(:milestone)) }
+ metrics = subject.metrics
+
+ expect(metrics).to be_present
+ expect(metrics.first_associated_with_milestone_at).to be_within(1.second).of(time)
+ end
+ end
+
+ context "list labels" do
+ it "records the first time an issue is associated with a list label" do
+ list_label = create(:label, lists: [create(:list)])
+ time = Time.now
+ Timecop.freeze(time) { subject.update(label_ids: [list_label.id]) }
+ metrics = subject.metrics
+
+ expect(metrics).to be_present
+ expect(metrics.first_added_to_board_at).to be_within(1.second).of(time)
+ end
+
+ it "does not record the second time an issue is associated with a list label" do
+ time = Time.now
+ first_list_label = create(:label, lists: [create(:list)])
+ Timecop.freeze(time) { subject.update(label_ids: [first_list_label.id]) }
+ second_list_label = create(:label, lists: [create(:list)])
+ Timecop.freeze(time + 5.hours) { subject.update(label_ids: [second_list_label.id]) }
+ metrics = subject.metrics
+
+ expect(metrics).to be_present
+ expect(metrics.first_added_to_board_at).to be_within(1.second).of(time)
+ end
+ end
+ end
+end
diff --git a/spec/models/merge_request/metrics_spec.rb b/spec/models/merge_request/metrics_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a79dd215d419a318d3a43398d3bc195b589606e8
--- /dev/null
+++ b/spec/models/merge_request/metrics_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe MergeRequest::Metrics, models: true do
+ let(:project) { create(:project) }
+
+ subject { create(:merge_request, source_project: project) }
+
+ describe "when recording the default set of metrics on merge request save" do
+ it "records the merge time" do
+ time = Time.now
+ Timecop.freeze(time) { subject.mark_as_merged }
+ metrics = subject.metrics
+
+ expect(metrics).to be_present
+ expect(metrics.merged_at).to be_within(1.second).of(time)
+ end
+ end
+end
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index e5b185dc3f642a0a11a95e75e7ddb6a8cc7a1b74..530a7def553935c5f7f40574bcba3e862bf6757d 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -64,5 +64,27 @@
end
end
end
+
+ describe '#commits_sha' do
+ shared_examples 'returning all commits SHA' do
+ it 'returns all commits SHA' do
+ commits_sha = subject.commits_sha
+
+ expect(commits_sha).to eq(subject.commits.map(&:sha))
+ end
+ end
+
+ context 'when commits were loaded' do
+ before do
+ subject.commits
+ end
+
+ it_behaves_like 'returning all commits SHA'
+ end
+
+ context 'when commits were not loaded' do
+ it_behaves_like 'returning all commits SHA'
+ end
+ end
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index c306ff938325543a9295b7dd4b7e7f897e002cd9..bed1285274a18a946c5f5a2861bbd2cd5db3c301 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -589,15 +589,77 @@
end
describe '#all_pipelines' do
- let!(:pipelines) do
- subject.merge_request_diff.commits.map do |commit|
- create(:ci_empty_pipeline, project: subject.source_project, sha: commit.id, ref: subject.source_branch)
+ shared_examples 'returning pipelines with proper ordering' do
+ let!(:all_pipelines) do
+ subject.all_commits_sha.map do |sha|
+ create(:ci_empty_pipeline,
+ project: subject.source_project,
+ sha: sha,
+ ref: subject.source_branch)
+ end
+ end
+
+ it 'returns all pipelines' do
+ expect(subject.all_pipelines).not_to be_empty
+ expect(subject.all_pipelines).to eq(all_pipelines.reverse)
+ end
+ end
+
+ context 'with single merge_request_diffs' do
+ it_behaves_like 'returning pipelines with proper ordering'
+ end
+
+ context 'with multiple irrelevant merge_request_diffs' do
+ before do
+ subject.update(target_branch: 'v1.0.0')
end
+
+ it_behaves_like 'returning pipelines with proper ordering'
end
- it 'returns a pipelines from source projects with proper ordering' do
- expect(subject.all_pipelines).not_to be_empty
- expect(subject.all_pipelines).to eq(pipelines.reverse)
+ context 'with unsaved merge request' do
+ subject { build(:merge_request) }
+
+ let!(:pipeline) do
+ create(:ci_empty_pipeline,
+ project: subject.project,
+ sha: subject.diff_head_sha,
+ ref: subject.source_branch)
+ end
+
+ it 'returns pipelines from diff_head_sha' do
+ expect(subject.all_pipelines).to contain_exactly(pipeline)
+ end
+ end
+ end
+
+ describe '#all_commits_sha' do
+ let(:all_commits_sha) do
+ subject.merge_request_diffs.flat_map(&:commits).map(&:sha).uniq
+ end
+
+ shared_examples 'returning all SHA' do
+ it 'returns all SHA from all merge_request_diffs' do
+ expect(subject.merge_request_diffs.size).to eq(2)
+ expect(subject.all_commits_sha).to eq(all_commits_sha)
+ end
+ end
+
+ context 'with a completely different branch' do
+ before do
+ subject.update(target_branch: 'v1.0.0')
+ end
+
+ it_behaves_like 'returning all SHA'
+ end
+
+ context 'with a branch having no difference' do
+ before do
+ subject.update(target_branch: 'v1.1.0')
+ subject.reload # make sure commits were not cached
+ end
+
+ it_behaves_like 'returning all SHA'
end
end
@@ -795,16 +857,57 @@
end
end
- describe "#environments" do
+ describe '#environments' do
let(:project) { create(:project) }
let(:merge_request) { create(:merge_request, source_project: project) }
- it 'selects deployed environments' do
- environments = create_list(:environment, 3, project: project)
- create(:deployment, environment: environments.first, sha: project.commit('master').id)
- create(:deployment, environment: environments.second, sha: project.commit('feature').id)
+ context 'with multiple environments' do
+ let(:environments) { create_list(:environment, 3, project: project) }
+
+ before do
+ create(:deployment, environment: environments.first, ref: 'master', sha: project.commit('master').id)
+ create(:deployment, environment: environments.second, ref: 'feature', sha: project.commit('feature').id)
+ end
+
+ it 'selects deployed environments' do
+ expect(merge_request.environments).to contain_exactly(environments.first)
+ end
+ end
+
+ context 'with environments on source project' do
+ let(:source_project) do
+ create(:project) do |fork_project|
+ fork_project.create_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
+ end
+ end
- expect(merge_request.environments).to eq [environments.first]
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: source_project, source_branch: 'feature',
+ target_project: project)
+ end
+
+ let(:source_environment) { create(:environment, project: source_project) }
+
+ before do
+ create(:deployment, environment: source_environment, ref: 'feature', sha: merge_request.diff_head_sha)
+ end
+
+ it 'selects deployed environments' do
+ expect(merge_request.environments).to contain_exactly(source_environment)
+ end
+
+ context 'with environments on target project' do
+ let(:target_environment) { create(:environment, project: project) }
+
+ before do
+ create(:deployment, environment: target_environment, tag: true, sha: merge_request.diff_head_sha)
+ end
+
+ it 'selects deployed environments' do
+ expect(merge_request.environments).to contain_exactly(source_environment, target_environment)
+ end
+ end
end
context 'without a diff_head_commit' do
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 544920d18240b4b902f213b1fc3c5346f130eb6a..0bebbef109e3868362a3a0bed0e88373786bad98 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -87,6 +87,18 @@
end
end
+ describe '#actual_size_limit' do
+ let(:namespace) { build(:namespace) }
+
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:repository_size_limit).and_return(50)
+ end
+
+ it 'returns the correct size limit' do
+ expect(namespace.actual_size_limit).to eq(50)
+ end
+ end
+
describe :rm_dir do
let!(:project) { create(:project, namespace: namespace) }
let!(:path) { File.join(Gitlab.config.repositories.storages.default, namespace.path) }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 9fb87fcc0689273fbcd64292f6487c23fa207833..8e63baafb9ce36ba41f1b0e230a85f5331266515 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -482,6 +482,76 @@
end
end
+ describe 'repository size restrictions' do
+ let(:project) { build(:empty_project) }
+
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:repository_size_limit).and_return(50)
+ end
+
+ describe '#changes_will_exceed_size_limit?' do
+ before do
+ allow(project).to receive(:repository_and_lfs_size).and_return(49)
+ end
+ it 'returns true when changes go over' do
+ expect(project.changes_will_exceed_size_limit?(5)).to be_truthy
+ end
+ end
+
+ describe '#actual_size_limit' do
+ it 'returns the limit set in the application settings' do
+ expect(project.actual_size_limit).to eq(50)
+ end
+
+ it 'returns the value set in the group' do
+ group = create(:group, repository_size_limit: 100)
+ project.update_attribute(:namespace_id, group.id)
+
+ expect(project.actual_size_limit).to eq(100)
+ end
+
+ it 'returns the value set locally' do
+ project.update_attribute(:repository_size_limit, 75)
+
+ expect(project.actual_size_limit).to eq(75)
+ end
+ end
+
+ describe '#size_limit_enabled?' do
+ it 'returns false when disabled' do
+ project.update_attribute(:repository_size_limit, 0)
+
+ expect(project.size_limit_enabled?).to be_falsey
+ end
+
+ it 'returns true when a limit is set' do
+ project.update_attribute(:repository_size_limit, 75)
+
+ expect(project.size_limit_enabled?).to be_truthy
+ end
+ end
+
+ describe '#above_size_limit?' do
+ it 'returns true when above the limit' do
+ allow(project).to receive(:repository_and_lfs_size).and_return(100)
+
+ expect(project.above_size_limit?).to be_truthy
+ end
+
+ it 'returns false when not over the limit' do
+ expect(project.above_size_limit?).to be_falsey
+ end
+ end
+
+ describe '#size_to_remove' do
+ it 'returns the correct value' do
+ allow(project).to receive(:repository_and_lfs_size).and_return(100)
+
+ expect(project.size_to_remove).to eq(50)
+ end
+ end
+ end
+
describe '#default_issues_tracker?' do
let(:project) { create(:project) }
let(:ext_project) { create(:redmine_project) }
@@ -1859,6 +1929,47 @@ def create_build(new_pipeline = pipeline, name = 'test')
end
end
+ describe '#environments_for' do
+ let(:project) { create(:project) }
+ let(:environment) { create(:environment, project: project) }
+
+ context 'tagged deployment' do
+ before do
+ create(:deployment, environment: environment, ref: '1.0', tag: true, sha: project.commit.id)
+ end
+
+ it 'returns environment when with_tags is set' do
+ expect(project.environments_for('master', project.commit, with_tags: true)).to contain_exactly(environment)
+ end
+
+ it 'does not return environment when no with_tags is set' do
+ expect(project.environments_for('master', project.commit)).to be_empty
+ end
+
+ it 'does not return environment when commit is not part of deployment' do
+ expect(project.environments_for('master', project.commit('feature'))).to be_empty
+ end
+ end
+
+ context 'branch deployment' do
+ before do
+ create(:deployment, environment: environment, ref: 'master', sha: project.commit.id)
+ end
+
+ it 'returns environment when ref is set' do
+ expect(project.environments_for('master', project.commit)).to contain_exactly(environment)
+ end
+
+ it 'does not environment when ref is different' do
+ expect(project.environments_for('feature', project.commit)).to be_empty
+ end
+
+ it 'does not return environment when commit is not part of deployment' do
+ expect(project.environments_for('master', project.commit('feature'))).to be_empty
+ end
+ end
+ end
+
def enable_lfs
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
end
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 0621c6a06ce68adef67afdbca84c9643992fe8cd..e6bc5296398046f96a55178791fd0f1c9c46bf4d 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -9,12 +9,14 @@
it { is_expected.to include_module(Participable) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
+ it { is_expected.to include_module(Awardable) }
end
describe 'associations' do
it { is_expected.to belong_to(:author).class_name('User') }
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
+ it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
end
describe 'validation' do
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index eda1cafd65e46ab12223d4f3945dd4c73b4495a8..a7a06744428bd853127dacc2fbdc8d4cdd0b90c7 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -33,4 +33,17 @@
it 'returns increasing permissions for each level' do
expect(users_permissions).to eq(users_permissions.sort.uniq)
end
+
+ it 'does not include the read_issue permission when the issue author is not a member of the private project' do
+ project = create(:project, :private)
+ issue = create(:issue, project: project)
+ user = issue.author
+
+ expect(project.team.member?(issue.author)).to eq(false)
+
+ expect(BasePolicy.class_for(project).abilities(user, project).can_set).
+ not_to include(:read_issue)
+
+ expect(Ability.allowed?(user, :read_issue, project)).to be_falsy
+ end
end
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 981a679188105809ec9cebb55c43c15a867fba47..5ad4fc4865aebf7cd14cbb29c6329011e50d7aa4 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -3,7 +3,7 @@
describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
- let!(:project) { create(:project) }
+ let!(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
@@ -39,6 +39,19 @@
end
end
+ context 'on a snippet' do
+ let(:snippet) { create(:project_snippet, :public, project: project) }
+ let!(:award) { create(:award_emoji, awardable: snippet) }
+
+ it 'returns the awarded emoji' do
+ get api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(award.name)
+ end
+ end
+
context 'when the user has no access' do
it 'returns a status code 404' do
user1 = create(:user)
@@ -91,6 +104,20 @@
end
end
+ context 'on a snippet' do
+ let(:snippet) { create(:project_snippet, :public, project: project) }
+ let!(:award) { create(:award_emoji, awardable: snippet) }
+
+ it 'returns the awarded emoji' do
+ get api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(award.name)
+ expect(json_response['awardable_id']).to eq(snippet.id)
+ expect(json_response['awardable_type']).to eq("Snippet")
+ end
+ end
+
context 'when the user has no access' do
it 'returns a status code 404' do
user1 = create(:user)
@@ -160,6 +187,18 @@
end
end
end
+
+ context 'on a snippet' do
+ it 'creates a new award emoji' do
+ snippet = create(:project_snippet, :public, project: project)
+
+ post api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user), name: 'blowfish'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['name']).to eq('blowfish')
+ expect(json_response['user']['username']).to eq(user.username)
+ end
+ end
end
describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do
@@ -229,6 +268,19 @@
expect(response).to have_http_status(404)
end
end
+
+ context 'when the awardable is a Snippet' do
+ let(:snippet) { create(:project_snippet, :public, project: project) }
+ let!(:award) { create(:award_emoji, awardable: snippet, user: user) }
+
+ it 'deletes the award' do
+ expect do
+ delete api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user)
+ end.to change { snippet.award_emoji.count }.from(1).to(0)
+
+ expect(response).to have_http_status(200)
+ end
+ end
end
describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_emoji_id' do
diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb
index 06e3a2183c0328326b35ae5a07e576c188f566e5..34f84f789525dc7e3c083f26e42094e75d4965ef 100644
--- a/spec/requests/api/fork_spec.rb
+++ b/spec/requests/api/fork_spec.rb
@@ -94,7 +94,7 @@
it 'fails if trying to fork to another user when not admin' do
post api("/projects/fork/#{project.id}", user2), namespace: admin.namespace.id
- expect(response).to have_http_status(409)
+ expect(response).to have_http_status(404)
end
it 'fails if trying to fork to non-existent namespace' do
@@ -114,7 +114,7 @@
it 'fails to fork to not owned group' do
post api("/projects/fork/#{project.id}", user2), namespace: group.name
- expect(response).to have_http_status(409)
+ expect(response).to have_http_status(404)
end
it 'forks to not owned group when admin' do
diff --git a/spec/requests/api/ldap_spec.rb b/spec/requests/api/ldap_spec.rb
index 81790f9d44c9fab7123333d36910978b3857ffea..4161989f38282fac275b9611e4d5f9590fca45ce 100644
--- a/spec/requests/api/ldap_spec.rb
+++ b/spec/requests/api/ldap_spec.rb
@@ -2,7 +2,10 @@
describe API::API do
include ApiHelpers
+ include LdapHelpers
+
let(:user) { create(:user) }
+ let(:adapter) { ldap_adapter }
before do
groups = [
@@ -10,7 +13,8 @@
OpenStruct.new(cn: 'students')
]
- allow_any_instance_of(Gitlab::LDAP::Adapter).to receive_messages(groups: groups)
+ allow(Gitlab::LDAP::Adapter).to receive(:new).and_return(adapter)
+ allow(adapter).to receive_messages(groups: groups)
end
describe "GET /ldap/groups" do
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index e8df2d9aa08e07c4e494e8f5a27451b172786bfc..79467f897270dc66a798105b1bfb3d3911aca00b 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -222,6 +222,22 @@
end
end
+ context "when repository is above size limit" do
+ let(:env) { { user: user.username, password: user.password } }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ it 'responds with status 403' do
+ allow_any_instance_of(Project).to receive(:above_size_limit?).and_return(true)
+
+ upload(path, env) do |response|
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
context "when username and password are provided" do
let(:env) { { user: user.username, password: 'nope' } }
@@ -423,7 +439,7 @@ def attempt_login(include_password)
project.team << [user, :reporter]
end
- shared_examples 'can download code only from own projects' do
+ shared_examples 'can download code only' do
it 'downloads get status 200' do
clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
@@ -441,7 +457,7 @@ def attempt_login(include_password)
context 'administrator' do
let(:user) { create(:admin) }
- it_behaves_like 'can download code only from own projects'
+ it_behaves_like 'can download code only'
it 'downloads from other project get status 403' do
clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
@@ -453,7 +469,7 @@ def attempt_login(include_password)
context 'regular user' do
let(:user) { create(:user) }
- it_behaves_like 'can download code only from own projects'
+ it_behaves_like 'can download code only'
it 'downloads from other project get status 404' do
clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 09e4e265dd15b9f6e4a9de1a35c82d9610d86f01..319d8f089f564f8f8916b220a2dfa78debe1cf49 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -634,7 +634,7 @@
{ 'operation' => 'upload',
'objects' => [
{ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
+ 'size' => 157507855
}]
}
end
@@ -646,10 +646,31 @@
it 'responds with upload hypermedia link' do
expect(json_response['objects']).to be_kind_of(Array)
expect(json_response['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897")
- expect(json_response['objects'].first['size']).to eq(1575078)
- expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078")
+ expect(json_response['objects'].first['size']).to eq(157507855)
+ expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/157507855")
expect(json_response['objects'].first['actions']['upload']['header']).to eq('Authorization' => authorization)
end
+
+ context 'and project is above the limit' do
+ let(:update_lfs_permissions) do
+ allow_any_instance_of(Project).to receive(:above_size_limit?).and_return(true)
+ end
+
+ it 'responds with status 406' do
+ expect(response).to have_http_status(406)
+ end
+ end
+
+ context 'and project will go over the limit' do
+ let(:update_lfs_permissions) do
+ allow_any_instance_of(Project).to receive_messages(actual_size_limit: 145, size_limit_enabled?: true)
+ end
+
+ it 'responds with status 406' do
+ expect(response).to have_http_status(406)
+ expect(json_response['documentation_url']).to include('/help')
+ end
+ end
end
context 'when pushing one new and one existing lfs object' do
diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb
index 41b897f36cd10486f9f9bf2ec5b5d412ecf1d0e5..343b4385bf25254e936107c58ccc3be08c09a0ac 100644
--- a/spec/services/create_deployment_service_spec.rb
+++ b/spec/services/create_deployment_service_spec.rb
@@ -169,4 +169,83 @@
end
end
end
+
+ describe "merge request metrics" do
+ let(:params) do
+ {
+ environment: 'production',
+ ref: 'master',
+ tag: false,
+ sha: '97de212e80737a608d939f648d959671fb0a0142b',
+ }
+ end
+
+ let(:merge_request) { create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: project) }
+
+ context "while updating the 'first_deployed_to_production_at' time" do
+ before { merge_request.mark_as_merged }
+
+ context "for merge requests merged before the current deploy" do
+ it "sets the time if the deploy's environment is 'production'" do
+ time = Time.now
+ Timecop.freeze(time) { service.execute }
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_within(1.second).of(time)
+ end
+
+ it "doesn't set the time if the deploy's environment is not 'production'" do
+ staging_params = params.merge(environment: 'staging')
+ service = described_class.new(project, user, staging_params)
+ service.execute
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil
+ end
+
+ it 'does not raise errors if the merge request does not have a metrics record' do
+ merge_request.metrics.destroy
+
+ expect(merge_request.reload.metrics).to be_nil
+ expect { service.execute }.not_to raise_error
+ end
+ end
+
+ context "for merge requests merged before the previous deploy" do
+ context "if the 'first_deployed_to_production_at' time is already set" do
+ it "does not overwrite the older 'first_deployed_to_production_at' time" do
+ # Previous deploy
+ time = Time.now
+ Timecop.freeze(time) { service.execute }
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_within(1.second).of(time)
+
+ # Current deploy
+ service = described_class.new(project, user, params)
+ Timecop.freeze(time + 12.hours) { service.execute }
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_within(1.second).of(time)
+ end
+ end
+
+ context "if the 'first_deployed_to_production_at' time is not already set" do
+ it "does not overwrite the older 'first_deployed_to_production_at' time" do
+ # Previous deploy
+ time = 5.minutes.from_now
+ Timecop.freeze(time) { service.execute }
+
+ expect(merge_request.reload.metrics.merged_at).to be < merge_request.reload.metrics.first_deployed_to_production_at
+
+ merge_request.reload.metrics.update(first_deployed_to_production_at: nil)
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil
+
+ # Current deploy
+ service = described_class.new(project, user, params)
+ Timecop.freeze(time + 12.hours) { service.execute }
+
+ expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 2a958009957ae4c41aeb86792d081baf15d96ee6..2582eff7ad5a451e53c4a019f455167771e01478 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -363,6 +363,43 @@
end
end
+ describe "issue metrics" do
+ let(:issue) { create :issue, project: project }
+ let(:commit_author) { create :user }
+ let(:commit) { project.commit }
+ let(:commit_time) { Time.now }
+
+ before do
+ project.team << [commit_author, :developer]
+ project.team << [user, :developer]
+
+ allow(commit).to receive_messages(
+ safe_message: "this commit \n mentions #{issue.to_reference}",
+ references: [issue],
+ author_name: commit_author.name,
+ author_email: commit_author.email,
+ committed_date: commit_time
+ )
+
+ allow(project.repository).to receive(:commits_between).and_return([commit])
+ end
+
+ context "while saving the 'first_mentioned_in_commit_at' metric for an issue" do
+ it 'sets the metric for referenced issues' do
+ execute_service(project, user, @oldrev, @newrev, @ref)
+
+ expect(issue.reload.metrics.first_mentioned_in_commit_at).to be_within(1.second).of(commit_time)
+ end
+
+ it 'does not set the metric for non-referenced issues' do
+ non_referenced_issue = create(:issue, project: project)
+ execute_service(project, user, @oldrev, @newrev, @ref)
+
+ expect(non_referenced_issue.reload.metrics.first_mentioned_in_commit_at).to be_nil
+ end
+ end
+ end
+
describe "closing issues from pushed commits containing a closing reference" do
let(:issue) { create :issue, project: project }
let(:other_issue) { create :issue, project: project }
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index c1e4f8bd96b019b8bb09c6b9445d2cc390ec9d54..b81428890756d6d902c90fa12251b6a1bc63a6a1 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -83,5 +83,34 @@
}
end
end
+
+ context 'while saving references to issues that the created merge request closes' do
+ let(:first_issue) { create(:issue, project: project) }
+ let(:second_issue) { create(:issue, project: project) }
+
+ let(:opts) do
+ {
+ title: 'Awesome merge_request',
+ source_branch: 'feature',
+ target_branch: 'master',
+ force_remove_source_branch: '1'
+ }
+ end
+
+ before do
+ project.team << [user, :master]
+ project.team << [assignee, :developer]
+ end
+
+ it 'creates a `MergeRequestsClosingIssues` record for each issue' do
+ issue_closing_opts = opts.merge(description: "Closes #{first_issue.to_reference} and #{second_issue.to_reference}")
+ service = described_class.new(project, user, issue_closing_opts)
+ allow(service).to receive(:execute_hooks)
+ merge_request = service.execute
+
+ issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+ expect(issue_ids).to match_array([first_issue.id, second_issue.id])
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 1d5c562eb8e6105c6138338213f5a7142d039bf1..5550683eb79ebc4af0f2741ce83ae3b1f118fa7b 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -38,6 +38,22 @@
end
end
+ context 'project has exceeded size limit' do
+ let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') }
+
+ before do
+ allow(project).to receive(:above_size_limit?).and_return(true)
+
+ perform_enqueued_jobs do
+ service.execute(merge_request)
+ end
+ end
+
+ it 'returns the correct error message' do
+ expect(merge_request.merge_error).to include('This merge request cannot be merged')
+ end
+ end
+
context 'remove source branch by author' do
let(:service) do
merge_request.merge_params['force_remove_source_branch'] = '1'
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index ede6d45dbf8e09052a37404c5d43d0e717f1d9b7..7c13017465022aee8adc8766c5f317a2a8a04ec6 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -249,6 +249,58 @@
end
end
+ context 'merge request metrics' do
+ let(:issue) { create :issue, project: @project }
+ let(:commit_author) { create :user }
+ let(:commit) { project.commit }
+
+ before do
+ project.team << [commit_author, :developer]
+ project.team << [user, :developer]
+
+ allow(commit).to receive_messages(
+ safe_message: "Closes #{issue.to_reference}",
+ references: [issue],
+ author_name: commit_author.name,
+ author_email: commit_author.email,
+ committed_date: Time.now
+ )
+
+ allow_any_instance_of(MergeRequest).to receive(:commits).and_return([commit])
+ end
+
+ context 'when the merge request is sourced from the same project' do
+ it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do
+ merge_request = create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: @project)
+ refresh_service = service.new(@project, @user)
+ allow(refresh_service).to receive(:execute_hooks)
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/feature')
+
+ issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+ expect(issue_ids).to eq([issue.id])
+ end
+ end
+
+ context 'when the merge request is sourced from a different project' do
+ it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do
+ forked_project = create(:project)
+ create(:forked_project_link, forked_to_project: forked_project, forked_from_project: @project)
+
+ merge_request = create(:merge_request,
+ target_branch: 'master',
+ source_branch: 'feature',
+ target_project: @project,
+ source_project: forked_project)
+ refresh_service = service.new(@project, @user)
+ allow(refresh_service).to receive(:execute_hooks)
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/feature')
+
+ issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+ expect(issue_ids).to eq([issue.id])
+ end
+ end
+ end
+
def reload_mrs
@merge_request.reload
@fork_merge_request.reload
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index a76e5f597da60eb2239986fdcd918c29a7c91944..7b7f7f0b67339478691e4d84c282cbaaddebddcb 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -315,5 +315,42 @@ def update_merge_request(opts)
end
end
end
+
+ context 'while saving references to issues that the updated merge request closes' do
+ let(:first_issue) { create(:issue, project: project) }
+ let(:second_issue) { create(:issue, project: project) }
+
+ it 'creates a `MergeRequestsClosingIssues` record for each issue' do
+ issue_closing_opts = { description: "Closes #{first_issue.to_reference} and #{second_issue.to_reference}" }
+ service = described_class.new(project, user, issue_closing_opts)
+ allow(service).to receive(:execute_hooks)
+ service.execute(merge_request)
+
+ issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+ expect(issue_ids).to match_array([first_issue.id, second_issue.id])
+ end
+
+ it 'removes `MergeRequestsClosingIssues` records when issues are not closed anymore' do
+ opts = {
+ title: 'Awesome merge_request',
+ description: "Closes #{first_issue.to_reference} and #{second_issue.to_reference}",
+ source_branch: 'feature',
+ target_branch: 'master',
+ force_remove_source_branch: '1'
+ }
+
+ merge_request = MergeRequests::CreateService.new(project, user, opts).execute
+
+ issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+ expect(issue_ids).to match_array([first_issue.id, second_issue.id])
+
+ service = described_class.new(project, user, description: "not closing any issues")
+ allow(service).to receive(:execute_hooks)
+ service.execute(merge_request.reload)
+
+ issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id)
+ expect(issue_ids).to be_empty
+ end
+ end
end
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 4ab4a6524860b279bc845a3121d50f335f23cea1..d3d98870c98ed7cf427f6066920ae372a2f6846d 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -379,6 +379,7 @@ def send_notifications(*new_mentions)
it "emails subscribers of the issue's labels" do
subscriber = create(:user)
label = create(:label, issues: [issue])
+ issue.reload
label.toggle_subscription(subscriber)
notification.new_issue(issue, @u_disabled)
@@ -399,6 +400,7 @@ def send_notifications(*new_mentions)
project.team << [guest, :guest]
label = create(:label, issues: [confidential_issue])
+ confidential_issue.reload
label.toggle_subscription(non_member)
label.toggle_subscription(author)
label.toggle_subscription(assignee)
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e8e760a618739d0720aca0de6d99a1b2cfb1941a
--- /dev/null
+++ b/spec/support/cycle_analytics_helpers.rb
@@ -0,0 +1,64 @@
+module CycleAnalyticsHelpers
+ def create_commit_referencing_issue(issue, branch_name: random_git_name)
+ project.repository.add_branch(user, branch_name, 'master')
+ create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name)
+ end
+
+ def create_commit(message, project, user, branch_name)
+ filename = random_git_name
+ oldrev = project.repository.commit(branch_name).sha
+
+ options = {
+ committer: project.repository.user_to_committer(user),
+ author: project.repository.user_to_committer(user),
+ commit: { message: message, branch: branch_name, update_ref: true },
+ file: { content: "content", path: filename, update: false }
+ }
+
+ commit_sha = Gitlab::Git::Blob.commit(project.repository, options)
+ project.repository.commit(commit_sha)
+
+ GitPushService.new(project,
+ user,
+ oldrev: oldrev,
+ newrev: commit_sha,
+ ref: 'refs/heads/master').execute
+ end
+
+ def create_merge_request_closing_issue(issue, message: nil, source_branch: nil)
+ if !source_branch || project.repository.commit(source_branch).blank?
+ source_branch = random_git_name
+ project.repository.add_branch(user, source_branch, 'master')
+ end
+
+ sha = project.repository.commit_file(user, random_git_name, "content", "commit message", source_branch, false)
+ project.repository.commit(sha)
+
+ opts = {
+ title: 'Awesome merge_request',
+ description: message || "Fixes #{issue.to_reference}",
+ source_branch: source_branch,
+ target_branch: 'master'
+ }
+
+ MergeRequests::CreateService.new(project, user, opts).execute
+ end
+
+ def merge_merge_requests_closing_issue(issue)
+ merge_requests = issue.closed_by_merge_requests
+ merge_requests.each { |merge_request| MergeRequests::MergeService.new(project, user).execute(merge_request) }
+ end
+
+ def deploy_master(environment: 'production')
+ CreateDeploymentService.new(project, user, {
+ environment: environment,
+ ref: 'master',
+ tag: false,
+ sha: project.repository.commit('master').sha
+ }).execute
+ end
+end
+
+RSpec.configure do |config|
+ config.include CycleAnalyticsHelpers
+end
diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8e19a6c92e2e29c7d5b6b1adbe39760da44ddc2e
--- /dev/null
+++ b/spec/support/cycle_analytics_helpers/test_generation.rb
@@ -0,0 +1,161 @@
+# rubocop:disable Metrics/AbcSize
+
+# Note: The ABC size is large here because we have a method generating test cases with
+# multiple nested contexts. This shouldn't count as a violation.
+
+module CycleAnalyticsHelpers
+ module TestGeneration
+ # Generate the most common set of specs that all cycle analytics phases need to have.
+ #
+ # Arguments:
+ #
+ # phase: Which phase are we testing? Will call `CycleAnalytics.new.send(phase)` for the final assertion
+ # data_fn: A function that returns a hash, constituting initial data for the test case
+ # start_time_conditions: An array of `conditions`. Each condition is an tuple of `condition_name` and `condition_fn`. `condition_fn` is called with
+ # `context` (no lexical scope, so need to do `context.create` for factories, for example) and `data` (from the `data_fn`).
+ # Each `condition_fn` is expected to implement a case which consitutes the start of the given cycle analytics phase.
+ # end_time_conditions: An array of `conditions`. Each condition is an tuple of `condition_name` and `condition_fn`. `condition_fn` is called with
+ # `context` (no lexical scope, so need to do `context.create` for factories, for example) and `data` (from the `data_fn`).
+ # Each `condition_fn` is expected to implement a case which consitutes the end of the given cycle analytics phase.
+ # before_end_fn: This function is run before calling the end time conditions. Used for setup that needs to be run between the start and end conditions.
+ # post_fn: Code that needs to be run after running the end time conditions.
+
+ def generate_cycle_analytics_spec(phase:, data_fn:, start_time_conditions:, end_time_conditions:, before_end_fn: nil, post_fn: nil)
+ combinations_of_start_time_conditions = (1..start_time_conditions.size).flat_map { |size| start_time_conditions.combination(size).to_a }
+ combinations_of_end_time_conditions = (1..end_time_conditions.size).flat_map { |size| end_time_conditions.combination(size).to_a }
+
+ scenarios = combinations_of_start_time_conditions.product(combinations_of_end_time_conditions)
+ scenarios.each do |start_time_conditions, end_time_conditions|
+ context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do
+ context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do
+ it "finds the median of available durations between the two conditions" do
+ time_differences = Array.new(5) do |index|
+ data = data_fn[self]
+ start_time = (index * 10).days.from_now
+ end_time = start_time + rand(1..5).days
+
+ start_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(start_time) { condition_fn[self, data] }
+ end
+
+ # Run `before_end_fn` at the midpoint between `start_time` and `end_time`
+ Timecop.freeze(start_time + (end_time - start_time) / 2) { before_end_fn[self, data] } if before_end_fn
+
+ end_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(end_time) { condition_fn[self, data] }
+ end
+
+ Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+
+ end_time - start_time
+ end
+
+ median_time_difference = time_differences.sort[2]
+ expect(subject.send(phase)).to be_within(5).of(median_time_difference)
+ end
+
+ context "when the data belongs to another project" do
+ let(:other_project) { create(:project) }
+
+ it "returns nil" do
+ # Use a stub to "trick" the data/condition functions
+ # into using another project. This saves us from having to
+ # define separate data/condition functions for this particular
+ # test case.
+ allow(self).to receive(:project) { other_project }
+
+ 5.times do
+ data = data_fn[self]
+ start_time = Time.now
+ end_time = rand(1..10).days.from_now
+
+ start_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(start_time) { condition_fn[self, data] }
+ end
+
+ end_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(end_time) { condition_fn[self, data] }
+ end
+
+ Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+ end
+
+ # Turn off the stub before checking assertions
+ allow(self).to receive(:project).and_call_original
+
+ expect(subject.send(phase)).to be_nil
+ end
+ end
+
+ context "when the end condition happens before the start condition" do
+ it 'returns nil' do
+ data = data_fn[self]
+ start_time = Time.now
+ end_time = start_time + rand(1..5).days
+
+ # Run `before_end_fn` at the midpoint between `start_time` and `end_time`
+ Timecop.freeze(start_time + (end_time - start_time) / 2) { before_end_fn[self, data] } if before_end_fn
+
+ end_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(start_time) { condition_fn[self, data] }
+ end
+
+ start_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(end_time) { condition_fn[self, data] }
+ end
+
+ Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+
+ expect(subject.send(phase)).to be_nil
+ end
+ end
+ end
+ end
+
+ context "start condition NOT PRESENT: #{start_time_conditions.map(&:first).to_sentence}" do
+ context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do
+ it "returns nil" do
+ 5.times do
+ data = data_fn[self]
+ end_time = rand(1..10).days.from_now
+
+ end_time_conditions.each_with_index do |(condition_name, condition_fn), index|
+ Timecop.freeze(end_time + index.days) { condition_fn[self, data] }
+ end
+
+ Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
+ end
+
+ expect(subject.send(phase)).to be_nil
+ end
+ end
+ end
+
+ context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do
+ context "end condition NOT PRESENT: #{end_time_conditions.map(&:first).to_sentence}" do
+ it "returns nil" do
+ 5.times do
+ data = data_fn[self]
+ start_time = Time.now
+
+ start_time_conditions.each do |condition_name, condition_fn|
+ Timecop.freeze(start_time) { condition_fn[self, data] }
+ end
+
+ post_fn[self, data] if post_fn
+ end
+
+ expect(subject.send(phase)).to be_nil
+ end
+ end
+ end
+ end
+
+ context "when none of the start / end conditions are matched" do
+ it "returns nil" do
+ expect(subject.send(phase)).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/ee/ldap_helpers.rb b/spec/support/ee/ldap_helpers.rb
index 590a63bd0440ac6cf2b97ebd2fbe23fc747307dc..4cec6841af2239118817931a96a8de1dcb17cc5e 100644
--- a/spec/support/ee/ldap_helpers.rb
+++ b/spec/support/ee/ldap_helpers.rb
@@ -46,7 +46,8 @@ def ldap_group_entry(
members,
cn: 'ldap_group1',
objectclass: 'groupOfNames',
- member_attr: 'uniqueMember'
+ member_attr: 'uniqueMember',
+ member_of: nil
)
entry = Net::LDAP::Entry.from_single_ldif_string(<<-EOS.strip_heredoc)
dn: cn=#{cn},ou=groups,dc=example,dc=com
@@ -56,9 +57,70 @@ def ldap_group_entry(
objectclass: #{objectclass}
EOS
+ entry['memberOf'] = member_of if member_of
members = [members].flatten
entry[member_attr] = members if members.any?
entry
end
+
+ # To simulate Active Directory ranged member retrieval. Create an LDAP
+ # group entry with any number of members in a given range. A '*' signifies
+ # the end of the 'pages' has been reached.
+ #
+ # Example:
+ # ldap_group_entry_with_member_range(
+ # [ 'user1', 'user2' ],
+ # cn: 'my_group',
+ # range_start: '0',
+ # range_end: '*'
+ # )
+ def ldap_group_entry_with_member_range(
+ members_in_range,
+ cn: 'ldap_group1',
+ range_start: '0',
+ range_end: '*'
+ )
+ entry = Net::LDAP::Entry.from_single_ldif_string(<<-EOS.strip_heredoc)
+ dn: cn=#{cn},ou=groups,dc=example,dc=com
+ cn: #{cn}
+ description: LDAP Group #{cn}
+ EOS
+
+ members_in_range = [members_in_range].flatten
+ entry["member;range=#{range_start}-#{range_end}"] = members_in_range
+ entry
+ end
+
+ # Stub Active Directory range member retrieval.
+ #
+ # Example:
+ # adapter = ::Gitlab::LDAP::Adapter.new('ldapmain', double(:ldap))
+ # group_entry_page1 = ldap_group_entry_with_member_range(
+ # [user_dn('user1'), user_dn('user2'), user_dn('user3')],
+ # range_start: '0',
+ # range_end: '2'
+ # )
+ # group_entry_page2 = ldap_group_entry_with_member_range(
+ # [user_dn('user4'), user_dn('user5'), user_dn('user6')],
+ # range_start: '3',
+ # range_end: '*'
+ # )
+ # group = EE::Gitlab::LDAP::Group.new(group_entry_page1, adapter)
+ #
+ # stub_ldap_adapter_group_members_in_range(group_entry_page2, adapter, range_start: '3')
+ def stub_ldap_adapter_group_members_in_range(
+ entry,
+ adapter = ldap_adapter,
+ range_start: '0'
+ )
+ allow(adapter).to receive(:group_members_in_range)
+ .with(entry.dn, range_start.to_i).and_return(entry)
+ end
+
+ def stub_ldap_adapter_nested_groups(parent_dn, entries = [], adapter = ldap_adapter)
+ groups = entries.map { |entry| EE::Gitlab::LDAP::Group.new(entry, adapter) }
+
+ allow(adapter).to receive(:nested_groups).with(parent_dn).and_return(groups)
+ end
end
end
diff --git a/spec/support/git_helpers.rb b/spec/support/git_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..93422390ef72ef3a094997663d137c302159396d
--- /dev/null
+++ b/spec/support/git_helpers.rb
@@ -0,0 +1,9 @@
+module GitHelpers
+ def random_git_name
+ "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}"
+ end
+end
+
+RSpec.configure do |config|
+ config.include GitHelpers
+end
diff --git a/spec/support/import_export/configuration_helper.rb b/spec/support/import_export/configuration_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f752508d48cfaef6cd801ca1f0cd7943d3be4625
--- /dev/null
+++ b/spec/support/import_export/configuration_helper.rb
@@ -0,0 +1,29 @@
+module ConfigurationHelper
+ # Returns a list of models from hashes/arrays contained in +project_tree+
+ def names_from_tree(project_tree)
+ project_tree.map do |branch_or_model|
+ branch_or_model = branch_or_model.to_s if branch_or_model.is_a?(Symbol)
+
+ branch_or_model.is_a?(String) ? branch_or_model : names_from_tree(branch_or_model)
+ end
+ end
+
+ def relation_class_for_name(relation_name)
+ relation_name = Gitlab::ImportExport::RelationFactory::OVERRIDES[relation_name.to_sym] || relation_name
+ relation_name.to_s.classify.constantize
+ end
+
+ def parsed_attributes(relation_name, attributes)
+ excluded_attributes = config_hash['excluded_attributes'][relation_name]
+ included_attributes = config_hash['included_attributes'][relation_name]
+
+ attributes = attributes - JSON[excluded_attributes.to_json] if excluded_attributes
+ attributes = attributes & JSON[included_attributes.to_json] if included_attributes
+
+ attributes
+ end
+
+ def associations_for(safe_model)
+ safe_model.reflect_on_all_associations.map { |assoc| assoc.name.to_s }
+ end
+end
diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..be0772d6a4a8f4abcceec771629e21066fc6bef5
--- /dev/null
+++ b/spec/support/import_export/export_file_helper.rb
@@ -0,0 +1,133 @@
+require './spec/support/import_export/configuration_helper'
+
+module ExportFileHelper
+ include ConfigurationHelper
+
+ ObjectWithParent = Struct.new(:object, :parent, :key_found)
+
+ def setup_project
+ project = create(:project, :public)
+
+ create(:release, project: project)
+
+ issue = create(:issue, assignee: user, project: project)
+ snippet = create(:project_snippet, project: project)
+ label = create(:label, project: project)
+ milestone = create(:milestone, project: project)
+ merge_request = create(:merge_request, source_project: project, milestone: milestone)
+ commit_status = create(:commit_status, project: project)
+
+ create(:label_link, label: label, target: issue)
+
+ ci_pipeline = create(:ci_pipeline,
+ project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch,
+ statuses: [commit_status])
+
+ create(:ci_build, pipeline: ci_pipeline, project: project)
+ create(:milestone, project: project)
+ create(:note, noteable: issue, project: project)
+ create(:note, noteable: merge_request, project: project)
+ create(:note, noteable: snippet, project: project)
+ create(:note_on_commit,
+ author: user,
+ project: project,
+ commit_id: ci_pipeline.sha)
+
+ create(:event, target: milestone, project: project, action: Event::CREATED, author: user)
+ create(:project_member, :master, user: user, project: project)
+ create(:ci_variable, project: project)
+ create(:ci_trigger, project: project)
+ key = create(:deploy_key)
+ key.projects << project
+ create(:service, project: project)
+ create(:project_hook, project: project, token: 'token')
+ create(:protected_branch, project: project)
+
+ project
+ end
+
+ # Expands the compressed file for an exported project into +tmpdir+
+ def in_directory_with_expanded_export(project)
+ Dir.mktmpdir do |tmpdir|
+ export_file = project.export_project_path
+ _output, exit_status = Gitlab::Popen.popen(%W{tar -zxf #{export_file} -C #{tmpdir}})
+
+ yield(exit_status, tmpdir)
+ end
+ end
+
+ # Recursively finds key/values including +key+ as part of the key, inside a nested hash
+ def deep_find_with_parent(sensitive_key_word, object, found = nil)
+ sensitive_key_found = object_contains_key?(object, sensitive_key_word)
+
+ # Returns the parent object and the object found containing a sensitive word as part of the key
+ if sensitive_key_found && object[sensitive_key_found]
+ ObjectWithParent.new(object[sensitive_key_found], object, sensitive_key_found)
+ elsif object.is_a?(Enumerable)
+ # Recursively lookup for keys containing sensitive words in a Hash or Array
+ object_with_parent = nil
+
+ object.find do |*hash_or_array|
+ object_with_parent = deep_find_with_parent(sensitive_key_word, hash_or_array.last, found)
+ end
+
+ object_with_parent
+ end
+ end
+
+ # Return true if the hash has a key containing a sensitive word
+ def object_contains_key?(object, sensitive_key_word)
+ return false unless object.is_a?(Hash)
+
+ object.keys.find { |key| key.include?(sensitive_key_word) }
+ end
+
+ # Returns the offended ObjectWithParent object if a sensitive word is found inside a hash,
+ # excluding the whitelisted safe hashes.
+ def find_sensitive_attributes(sensitive_word, project_hash)
+ loop do
+ object_with_parent = deep_find_with_parent(sensitive_word, project_hash)
+
+ return nil unless object_with_parent && object_with_parent.object
+
+ if is_safe_hash?(object_with_parent.parent, sensitive_word)
+ # It's in the safe list, remove hash and keep looking
+ object_with_parent.parent.delete(object_with_parent.key_found)
+ else
+ return object_with_parent
+ end
+
+ nil
+ end
+ end
+
+ # Returns true if it's one of the excluded models in +safe_list+
+ def is_safe_hash?(parent, sensitive_word)
+ return false unless parent && safe_list[sensitive_word.to_sym]
+
+ # Extra attributes that appear in a model but not in the exported hash.
+ excluded_attributes = ['type']
+
+ safe_list[sensitive_word.to_sym].each do |model|
+ # Check whether this is a hash attribute inside a model
+ if model.is_a?(Symbol)
+ return true if (safe_hashes[model] - parent.keys).empty?
+ else
+ return true if safe_model?(model, excluded_attributes, parent)
+ end
+ end
+
+ false
+ end
+
+ # Compares model attributes with those those found in the hash
+ # and returns true if there is a match, ignoring some excluded attributes.
+ def safe_model?(model, excluded_attributes, parent)
+ excluded_attributes += associations_for(model)
+ parsed_model_attributes = parsed_attributes(model.name.underscore, model.attribute_names)
+
+ (parsed_model_attributes - parent.keys - excluded_attributes).empty?
+ end
+end
diff --git a/spec/views/projects/merge_requests/_heading.html.haml_spec.rb b/spec/views/projects/merge_requests/_heading.html.haml_spec.rb
index 733b2dfa7ffe9c30e7441b0714fe945112503175..21f49d396e7b530ddb5244f87403779433146d97 100644
--- a/spec/views/projects/merge_requests/_heading.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/_heading.html.haml_spec.rb
@@ -15,6 +15,8 @@
assign(:merge_request, merge_request)
assign(:project, project)
+ allow(view).to receive(:can?).and_return(true)
+
render
end