Commit 517effe9 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into show-commit-status-from-source-project

* upstream/master: (368 commits)
  Use root_url for issue boards user link
  removes extra line for empty milestone description
  code formatting corrected
  Fix reply-by-email not working due to queue name mismatch
  Remove duplicate code in repository cache clearing
  Expire and build repository cache after project import
  refactor(email): use setter method instead AR callbacks
  Move spec/mailers/shared/notify.rb to spec/support
  Fix status code expectation
  Stop clearing the database cache on rake cache:clear
  Fix error in generating labels
  Fix bug where e-mails were not being sent out via Sidekiq
  Document link syntax introduced by !5586
  Fix documents and comments on Build API `scope`. #23146 #19131
  adds entry in CHANGELOG
  removes extra line for empty issue description
  Re-organize queues to use for Sidekiq
  Fix wrong endpoint in api/users documentation, fix same typo in spec describe blocks
  Update CHANGELOG
  Fix object data to be sent to fetch analytics data
  ...
parents 68f964ad 4028022f
...@@ -5,7 +5,7 @@ require: ...@@ -5,7 +5,7 @@ require:
inherit_from: .rubocop_todo.yml inherit_from: .rubocop_todo.yml
AllCops: AllCops:
TargetRubyVersion: 2.3 TargetRubyVersion: 2.1
# Cop names are not d§splayed in offense messages by default. Change behavior # Cop names are not d§splayed in offense messages by default. Change behavior
# by overriding DisplayCopNames, or by giving the -D/--display-cop-names # by overriding DisplayCopNames, or by giving the -D/--display-cop-names
# option. # option.
......
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
## 8.13.0 (2016-10-22) ## 8.14.0 (2016-11-22)
- Adds user project membership expired event to clarify why user was removed (Callum Dryden)
- Trim leading and trailing whitespace on project_path (Linus Thiel)
- Fix HipChat notifications rendering (airatshigapov, eisnerd)
- Add hover to trash icon in notes !7008 (blackst0ne)
- Simpler arguments passed to named_route on toggle_award_url helper method
- Fix: Backup restore doesn't clear cache
- Use MergeRequestsClosingIssues cache data on Issue#closed_by_merge_requests method
- Fix documents and comments on Build API `scope`
- Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov)
## 8.13.1 (unreleased)
- Fix error in generating labels
- Fix reply-by-email not working due to queue name mismatch
- Expire and build repository cache after project import
## 8.13.0 (2016-10-22)
- Removes extra line for empty issue description. (!7045)
- Fix save button on project pipeline settings page. (!6955)
- All Sidekiq workers now use their own queue
- Avoid race condition when asynchronously removing expired artifacts. (!6881)
- Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675) - Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675)
- Respond with 404 Not Found for non-existent tags (Linus Thiel) - Respond with 404 Not Found for non-existent tags (Linus Thiel)
- Truncate long labels with ellipsis in labels page - Truncate long labels with ellipsis in labels page
...@@ -11,25 +30,36 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -11,25 +30,36 @@ Please view this file on the master branch, on stable branches it's out of date.
- Update runner version only when updating contacted_at - Update runner version only when updating contacted_at
- Add link from system note to compare with previous version - Add link from system note to compare with previous version
- Use gitlab-shell v3.6.6 - Use gitlab-shell v3.6.6
- Ignore references to internal issues when using external issues tracker
- Ability to resolve merge request conflicts with editor !6374 - Ability to resolve merge request conflicts with editor !6374
- Add `/projects/visible` API endpoint (Ben Boeckel) - Add `/projects/visible` API endpoint (Ben Boeckel)
- Fix centering of custom header logos (Ashley Dumaine) - Fix centering of custom header logos (Ashley Dumaine)
- Keep around commits only pipeline creation as pipeline data doesn't change over time
- Update duration at the end of pipeline
- ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup - ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
- Add group level labels. (!6425)
- Fix Cycle analytics not showing correct data when filtering by date. !6906
- Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun) - Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun)
- Cancelled pipelines could be retried. !6927
- Updating verbiage on git basics to be more intuitive - Updating verbiage on git basics to be more intuitive
- Fix project_feature record not generated on project creation
- Clarify documentation for Runners API (Gennady Trafimenkov) - Clarify documentation for Runners API (Gennady Trafimenkov)
- The instrumentation for Banzai::Renderer has been restored - The instrumentation for Banzai::Renderer has been restored
- Change user & group landing page routing from /u/:username to /:username - Change user & group landing page routing from /u/:username to /:username
- Fixed issue boards user link when in subdirectory
- Added documentation for .gitattributes files - Added documentation for .gitattributes files
- Move Pipeline Metrics to separate worker - Move Pipeline Metrics to separate worker
- AbstractReferenceFilter caches project_refs on RequestStore when active - AbstractReferenceFilter caches project_refs on RequestStore when active
- Replaced the check sign to arrow in the show build view. !6501 - Replaced the check sign to arrow in the show build view. !6501
- Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar) - Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar)
- ProjectCacheWorker updates caches at most once per 15 minutes per project
- Fix Error 500 when viewing old merge requests with bad diff data - Fix Error 500 when viewing old merge requests with bad diff data
- Create a new /templates namespace for the /licenses, /gitignores and /gitlab_ci_ymls API endpoints. !5717 (tbalthazar) - Create a new /templates namespace for the /licenses, /gitignores and /gitlab_ci_ymls API endpoints. !5717 (tbalthazar)
- Fix viewing merged MRs when the source project has been removed !6991
- Speed-up group milestones show page - Speed-up group milestones show page
- Fix inconsistent options dropdown caret on mobile viewports (ClemMakesApps) - Fix inconsistent options dropdown caret on mobile viewports (ClemMakesApps)
- Extract project#update_merge_requests and SystemHooks to its own worker from GitPushService - Extract project#update_merge_requests and SystemHooks to its own worker from GitPushService
- Fix discussion thread from emails for merge requests. !7010
- Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs) - Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs)
- Add tag shortcut from the Commit page. !6543 - Add tag shortcut from the Commit page. !6543
- Keep refs for each deployment - Keep refs for each deployment
...@@ -53,6 +83,7 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -53,6 +83,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Add Issue Board API support (andrebsguedes) - Add Issue Board API support (andrebsguedes)
- Allow the Koding integration to be configured through the API - Allow the Koding integration to be configured through the API
- Add new issue button to each list on Issues Board - Add new issue button to each list on Issues Board
- Execute specific named route method from toggle_award_url helper method
- Added soft wrap button to repository file/blob editor - Added soft wrap button to repository file/blob editor
- Update namespace validation to forbid reserved names (.git and .atom) (Will Starms) - Update namespace validation to forbid reserved names (.git and .atom) (Will Starms)
- Show the time ago a merge request was deployed to an environment - Show the time ago a merge request was deployed to an environment
...@@ -62,6 +93,7 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -62,6 +93,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps) - Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps)
- Remove redundant mixins (ClemMakesApps) - Remove redundant mixins (ClemMakesApps)
- Added 'Download' button to the Snippets page (Justin DiPierro) - Added 'Download' button to the Snippets page (Justin DiPierro)
- Add visibility level to project repository
- Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison) - Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
- Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska) - Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska)
- Fix showing commits from source project for merge request !6658 - Fix showing commits from source project for merge request !6658
...@@ -101,10 +133,10 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -101,10 +133,10 @@ Please view this file on the master branch, on stable branches it's out of date.
- Optimize GitHub importing for speed and memory - Optimize GitHub importing for speed and memory
- API: expose pipeline data in builds API (!6502, Guilherme Salazar) - API: expose pipeline data in builds API (!6502, Guilherme Salazar)
- Notify the Merger about merge after successful build (Dimitris Karakasilis) - Notify the Merger about merge after successful build (Dimitris Karakasilis)
- Reorder issue and merge request titles to show IDs first. !6503 (Greg Laubenstein)
- Reduce queries needed to find users using their SSH keys when pushing commits - Reduce queries needed to find users using their SSH keys when pushing commits
- Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska) - Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska)
- Fix broken repository 500 errors in project list - Fix broken repository 500 errors in project list
- Fix the diff in the merge request view when converting a symlink to a regular file
- Fix Pipeline list commit column width should be adjusted - Fix Pipeline list commit column width should be adjusted
- Close todos when accepting merge requests via the API !6486 (tonygambone) - Close todos when accepting merge requests via the API !6486 (tonygambone)
- Ability to batch assign issues relating to a merge request to the author. !5725 (jamedjo) - Ability to batch assign issues relating to a merge request to the author. !5725 (jamedjo)
...@@ -112,14 +144,20 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -112,14 +144,20 @@ Please view this file on the master branch, on stable branches it's out of date.
- Retouch environments list and deployments list - Retouch environments list and deployments list
- Add multiple command support for all label related slash commands !6780 (barthc) - Add multiple command support for all label related slash commands !6780 (barthc)
- Add Container Registry on/off status to Admin Area !6638 (the-undefined) - Add Container Registry on/off status to Admin Area !6638 (the-undefined)
- Add Nofollow for uppercased scheme in external urls !6820 (the-undefined)
- Allow empty merge requests !6384 (Artem Sidorenko) - Allow empty merge requests !6384 (Artem Sidorenko)
- Grouped pipeline dropdown is a scrollable container - Grouped pipeline dropdown is a scrollable container
- Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi) - Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi)
- Fixes padding in all clipboard icons that have .btn class - Fixes padding in all clipboard icons that have .btn class
- Fix a typo in doc/api/labels.md - Fix a typo in doc/api/labels.md
- Fix double-escaping in activities tab (Alexandre Maia)
- API: all unknown routing will be handled with 404 Not Found - API: all unknown routing will be handled with 404 Not Found
- Add docs for request profiling - Add docs for request profiling
- Delete dynamic environments
- Fix buggy iOS tooltip layering behavior.
- Make guests unable to view MRs on private projects - Make guests unable to view MRs on private projects
- Fix broken Project API docs (Takuya Noguchi)
- Migrate invalid project members (owner -> master)
## 8.12.7 ## 8.12.7
...@@ -245,6 +283,7 @@ Please view this file on the master branch, on stable branches it's out of date. ...@@ -245,6 +283,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Changed MR widget build status to pipeline status !6335 - Changed MR widget build status to pipeline status !6335
- Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel) - Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
- Enable pipeline events by default !6278 - Enable pipeline events by default !6278
- Add pipeline email service !6019
- Move parsing of sidekiq ps into helper !6245 (pascalbetz) - Move parsing of sidekiq ps into helper !6245 (pascalbetz)
- Added go to issue boards keyboard shortcut - Added go to issue boards keyboard shortcut
- Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel) - Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel)
......
...@@ -29,7 +29,7 @@ gem 'omniauth-github', '~> 1.1.1' ...@@ -29,7 +29,7 @@ gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.0' gem 'omniauth-gitlab', '~> 1.0.0'
gem 'omniauth-google-oauth2', '~> 0.4.1' gem 'omniauth-google-oauth2', '~> 0.4.1'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-saml', '~> 1.6.0' gem 'omniauth-saml', '~> 1.7.0'
gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0' gem 'omniauth_crowd', '~> 2.2.0'
......
...@@ -473,9 +473,9 @@ GEM ...@@ -473,9 +473,9 @@ GEM
omniauth-oauth2 (1.3.1) omniauth-oauth2 (1.3.1)
oauth2 (~> 1.0) oauth2 (~> 1.0)
omniauth (~> 1.2) omniauth (~> 1.2)
omniauth-saml (1.6.0) omniauth-saml (1.7.0)
omniauth (~> 1.3) omniauth (~> 1.3)
ruby-saml (~> 1.3) ruby-saml (~> 1.4)
omniauth-shibboleth (1.2.1) omniauth-shibboleth (1.2.1)
omniauth (>= 1.0.0) omniauth (>= 1.0.0)
omniauth-twitter (1.2.1) omniauth-twitter (1.2.1)
...@@ -635,7 +635,7 @@ GEM ...@@ -635,7 +635,7 @@ GEM
crack (~> 0.4) crack (~> 0.4)
ruby-prof (0.16.2) ruby-prof (0.16.2)
ruby-progressbar (1.8.1) ruby-progressbar (1.8.1)
ruby-saml (1.3.0) ruby-saml (1.4.1)
nokogiri (>= 1.5.10) nokogiri (>= 1.5.10)
ruby_parser (3.8.2) ruby_parser (3.8.2)
sexp_processor (~> 4.1) sexp_processor (~> 4.1)
...@@ -915,7 +915,7 @@ DEPENDENCIES ...@@ -915,7 +915,7 @@ DEPENDENCIES
omniauth-gitlab (~> 1.0.0) omniauth-gitlab (~> 1.0.0)
omniauth-google-oauth2 (~> 0.4.1) omniauth-google-oauth2 (~> 0.4.1)
omniauth-kerberos (~> 0.3.0) omniauth-kerberos (~> 0.3.0)
omniauth-saml (~> 1.6.0) omniauth-saml (~> 1.7.0)
omniauth-shibboleth (~> 1.2.0) omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0) omniauth-twitter (~> 1.2.0)
omniauth_crowd (~> 2.2.0) omniauth_crowd (~> 2.2.0)
......
8.13.0-pre 8.14.0-pre
//= require vue
((global) => { ((global) => {
const COOKIE_NAME = 'cycle_analytics_help_dismissed'; const COOKIE_NAME = 'cycle_analytics_help_dismissed';
...@@ -34,7 +36,11 @@ ...@@ -34,7 +36,11 @@
method: 'GET', method: 'GET',
dataType: 'json', dataType: 'json',
contentType: 'application/json', contentType: 'application/json',
data: { start_date: options.startDate } data: {
cycle_analytics: {
start_date: options.startDate
}
}
}).done((data) => { }).done((data) => {
this.decorateData(data); this.decorateData(data);
this.initDropdown(); this.initDropdown();
......
...@@ -168,6 +168,8 @@ ...@@ -168,6 +168,8 @@
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new ShortcutsBlob(true); new ShortcutsBlob(true);
break; break;
case 'groups:labels:new':
case 'groups:labels:edit':
case 'projects:labels:new': case 'projects:labels:new':
case 'projects:labels:edit': case 'projects:labels:edit':
new Labels(); new Labels();
......
...@@ -266,7 +266,7 @@ ...@@ -266,7 +266,7 @@
}, },
fieldName: $dropdown.data('field-name'), fieldName: $dropdown.data('field-name'),
id: function(label) { id: function(label) {
if (label.id <= 0) return; if (label.id <= 0) return label.title;
if ($dropdown.hasClass('js-issuable-form-dropdown')) { if ($dropdown.hasClass('js-issuable-form-dropdown')) {
return label.id; return label.id;
......
...@@ -388,28 +388,25 @@ ...@@ -388,28 +388,25 @@
// So we dont affix the tabs on these // So we dont affix the tabs on these
if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return; if (Breakpoints.get().getBreakpointSize() === 'xs' || !$tabs.length) return;
var tabsWidth = $tabs.outerWidth(), var $diffTabs = $('#diff-notes-app'),
$diffTabs = $('#diff-notes-app'), $fixedNav = $('.navbar-fixed-top'),
offsetTop = $tabs.offset().top - ($('.navbar-fixed-top').height() + $('.layout-nav').height()); $layoutNav = $('.layout-nav');
$tabs.off('affix.bs.affix affix-top.bs.affix') $tabs.off('affix.bs.affix affix-top.bs.affix')
.affix({ .affix({
offset: { offset: {
top: offsetTop top: function () {
var tabsTop = $diffTabs.offset().top - $tabs.height();
tabsTop = tabsTop - ($fixedNav.height() + $layoutNav.height());
return tabsTop;
}
} }
}).on('affix.bs.affix', function () { }).on('affix.bs.affix', function () {
$tabs.css({
left: $tabs.offset().left,
width: tabsWidth
});
$diffTabs.css({ $diffTabs.css({
marginTop: $tabs.height() marginTop: $tabs.height()
}); });
}).on('affix-top.bs.affix', function () { }).on('affix-top.bs.affix', function () {
$tabs.css({
left: '',
width: ''
});
$diffTabs.css({ $diffTabs.css({
marginTop: '' marginTop: ''
}); });
......
...@@ -17,6 +17,12 @@ ...@@ -17,6 +17,12 @@
View on <%- external_url_formatted %> View on <%- external_url_formatted %>
</a> </a>
</span> </span>
<span class="stop-env-container js-stop-env-link">
<a href="<%- stop_url %>" class="close-evn-link" data-method="post" rel="nofollow" data-confirm="Are you sure you want to stop this environment?">
<i class="fa fa-stop-circle-o"/>
Stop environment
</a>
</span>
</div> </div>
</div>`; </div>`;
...@@ -205,6 +211,11 @@ ...@@ -205,6 +211,11 @@
if ($(`.mr-state-widget #${ environment.id }`).length) return; if ($(`.mr-state-widget #${ environment.id }`).length) return;
const $template = $(DEPLOYMENT_TEMPLATE); const $template = $(DEPLOYMENT_TEMPLATE);
if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove(); if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove();
if (!environment.stop_url) {
$('.js-stop-env-link', $template).remove();
}
if (environment.deployed_at && environment.deployed_at_formatted) { if (environment.deployed_at && environment.deployed_at_formatted) {
environment.deployed_at = $.timeago(environment.deployed_at) + '.'; environment.deployed_at = $.timeago(environment.deployed_at) + '.';
} else { } else {
......
...@@ -4,9 +4,8 @@ ...@@ -4,9 +4,8 @@
this.ProjectNew = (function() { this.ProjectNew = (function() {
function ProjectNew() { function ProjectNew() {
this.toggleSettings = bind(this.toggleSettings, this); this.toggleSettings = bind(this.toggleSettings, this);
this.$selects = $('.features select').filter(function () { this.$selects = $('.features select');
return $(this).data('field'); this.$repoSelects = this.$selects.filter('.js-repo-select');
});
$('.project-edit-container').on('ajax:before', (function(_this) { $('.project-edit-container').on('ajax:before', (function(_this) {
return function() { return function() {
...@@ -16,6 +15,7 @@ ...@@ -16,6 +15,7 @@
})(this)); })(this));
this.toggleSettings(); this.toggleSettings();
this.toggleSettingsOnclick(); this.toggleSettingsOnclick();
this.toggleRepoVisibility();
} }
ProjectNew.prototype.toggleSettings = function() { ProjectNew.prototype.toggleSettings = function() {
...@@ -43,6 +43,38 @@ ...@@ -43,6 +43,38 @@
} }
}; };
ProjectNew.prototype.toggleRepoVisibility = function () {
var $repoAccessLevel = $('.js-repo-access-level select');
this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
.nextAll()
.hide();
$repoAccessLevel.off('change')
.on('change', function () {
var selectedVal = parseInt($repoAccessLevel.val());
this.$repoSelects.each(function () {
var $this = $(this),
repoSelectVal = parseInt($this.val());
$this.find('option').show();
if (selectedVal < repoSelectVal) {
$this.val(selectedVal);
}
$this.find("option[value='" + selectedVal + "']").nextAll().hide();
});
if (selectedVal) {
this.$repoSelects.removeClass('disabled');
} else {
this.$repoSelects.addClass('disabled');
}
}.bind(this));
};
return ProjectNew; return ProjectNew;
})(); })();
......
...@@ -372,3 +372,5 @@ table { ...@@ -372,3 +372,5 @@ table {
margin-right: -$gl-padding; margin-right: -$gl-padding;
border-top: 1px solid $border-color; border-top: 1px solid $border-color;
} }
.hide-bottom-border { border-bottom: none !important; }
...@@ -167,7 +167,6 @@ ...@@ -167,7 +167,6 @@
*/ */
&.code { &.code {
padding: 0; padding: 0;
-webkit-overflow-scrolling: auto; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
} }
} }
} }
......
...@@ -27,3 +27,15 @@ body { ...@@ -27,3 +27,15 @@ body {
.container-limited { .container-limited {
max-width: $fixed-layout-width; max-width: $fixed-layout-width;
} }
/* The following prevents side effects related to iOS Safari's implementation of -webkit-overflow-scrolling: touch,
which is applied to the body by jquery.nicescroling plugin to force hardware acceleration for momentum scrolling. Side
effects are commonly related to inconsisent z-index behavior (e.g. tooltips). By applying the following to direct children
of the body element here, we negate cascading side effects but allow momentum scrolling to be applied to the body */
.navbar,
.page-gutter,
.page-with-sidebar {
-webkit-overflow-scrolling: auto;
}
...@@ -185,6 +185,10 @@ header.header-sidebar-pinned { ...@@ -185,6 +185,10 @@ header.header-sidebar-pinned {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
padding-right: $sidebar_collapsed_width; padding-right: $sidebar_collapsed_width;
.merge-request-tabs-holder.affix {
right: $sidebar_collapsed_width;
}
} }
.sidebar-collapsed-icon { .sidebar-collapsed-icon {
...@@ -207,6 +211,10 @@ header.header-sidebar-pinned { ...@@ -207,6 +211,10 @@ header.header-sidebar-pinned {
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
padding-right: $gutter_width; padding-right: $gutter_width;
.merge-request-tabs-holder.affix {
right: $gutter_width;
}
} }
&.with-overlay { &.with-overlay {
......
...@@ -16,6 +16,7 @@ $white-light: #fff; ...@@ -16,6 +16,7 @@ $white-light: #fff;
$white-normal: #ededed; $white-normal: #ededed;
$white-dark: #ececec; $white-dark: #ececec;
$gray-lightest: #fdfdfd;
$gray-light: #fafafa; $gray-light: #fafafa;
$gray-lighter: #f9f9f9; $gray-lighter: #f9f9f9;
$gray-normal: #f5f5f5; $gray-normal: #f5f5f5;
......
lex
[v-cloak] { [v-cloak] {
display: none; display: none;
} }
...@@ -132,7 +131,7 @@ lex ...@@ -132,7 +131,7 @@ lex
} }
.board-blank-state { .board-blank-state {
height: 100%; height: calc(100% - 49px);
padding: $gl-padding; padding: $gl-padding;
background-color: #fff; background-color: #fff;
} }
......
...@@ -195,7 +195,7 @@ ...@@ -195,7 +195,7 @@
.build-job { .build-job {
position: relative; position: relative;
.fa { .fa-arrow-right {
position: absolute; position: absolute;
left: 15px; left: 15px;
top: 20px; top: 20px;
...@@ -205,14 +205,23 @@ ...@@ -205,14 +205,23 @@
&.active { &.active {
font-weight: bold; font-weight: bold;
.fa { .fa-arrow-right {
display: block; display: block;
} }
} }
&.retried {
background-color: $gray-lightest;
}
&:hover { &:hover {
background-color: $row-hover; background-color: $row-hover;
} }
.fa-refresh {
font-size: 13px;
margin-left: 3px;
}
} }
} }
} }
......
...@@ -52,7 +52,6 @@ ...@@ -52,7 +52,6 @@
background: #fff; background: #fff;
color: #333; color: #333;
border-radius: 0 0 3px 3px; border-radius: 0 0 3px 3px;
-webkit-overflow-scrolling: auto;
.unfold { .unfold {
cursor: pointer; cursor: pointer;
......
...@@ -38,6 +38,14 @@ ...@@ -38,6 +38,14 @@
color: $gl-dark-link-color; color: $gl-dark-link-color;
} }
.stop-env-link {
color: $table-text-gray;
.stop-env-icon {
font-size: 14px;
}
}
.deployment { .deployment {
.build-column { .build-column {
......
...@@ -69,6 +69,20 @@ ...@@ -69,6 +69,20 @@
} }
} }
.label-type {
display: block;
margin-bottom: 10px;
margin-left: 50px;
@media (min-width: $screen-sm-min) {
display: inline-block;
width: 100px;
margin-left: 10px;
margin-bottom: 0;
vertical-align: middle;
}
}
.label-description { .label-description {
display: block; display: block;
margin-bottom: 10px; margin-bottom: 10px;
...@@ -209,6 +223,13 @@ ...@@ -209,6 +223,13 @@
} }
.label-subscribe-button { .label-subscribe-button {
.label-subscribe-button-icon {
&[disabled] {
opacity: 0.5;
pointer-events: none;
}
}
.label-subscribe-button-loading { .label-subscribe-button-loading {
display: none; display: none;
} }
......
...@@ -53,6 +53,7 @@ ...@@ -53,6 +53,7 @@
margin: 0 0 10px; margin: 0 0 10px;
} }
.login-footer { .login-footer {
margin-top: 10px; margin-top: 10px;
...@@ -124,19 +125,25 @@ ...@@ -124,19 +125,25 @@
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
box-shadow: 0 0 0 1px $border-color; box-shadow: 0 0 0 1px $border-color;
border-top-right-radius: 2px; border-top-right-radius: $border-radius-default;
border-top-left-radius: 2px; border-top-left-radius: $border-radius-default;
li { li {
flex: 1; flex: 1;
text-align: center; text-align: center;
&:first-of-type {
border-top-left-radius: $border-radius-default;
}
&:last-of-type { &:last-of-type {
border-left: 1px solid $border-color; border-left: 1px solid $border-color;
border-top-right-radius: $border-radius-default;
} }
&:not(.active) { &:not(.active) {
background-color: $gray-light; background-color: $gray-light;
border-left: 1px solid $border-color;
} }
a { a {
...@@ -164,6 +171,31 @@ ...@@ -164,6 +171,31 @@
} }
} }
// Ldap configurations may need more tabs & the tab labels are user generated (arbitrarily long).
// These styles prevent this from breaking the layout, and only applied when providers are configured.
.new-session-tabs.custom-provider-tabs {
flex-wrap: wrap;
li {
min-width: 85px;
flex-basis: auto;
// This styles tab elements that have wrapped to a second line. We cannot easily predict when this will happen.
// We are making somewhat of an assumption about the configuration here: that users do not have more than
// 3 LDAP servers configured (in addition to standard login) and they are not using especially long names for any
// of them. If either condition is false, this will work as expected. If both are true, there may be a missing border
// above one of the bottom row elements. If you know a better way, please implement it!
&:nth-child(n+5) {
border-top: 1px solid $border-color;
}
}
a {
font-size: 16px;
}
}
.form-control { .form-control {
&:active, &:focus { &:active, &:focus {
...@@ -197,6 +229,7 @@ ...@@ -197,6 +229,7 @@
.login-page { .login-page {
.col-sm-5.pull-right { .col-sm-5.pull-right {
float: none !important; float: none !important;
margin-bottom: 45px;
} }
} }
} }
...@@ -238,6 +271,27 @@ ...@@ -238,6 +271,27 @@
} }
.navless-container { .navless-container {
padding: 65px; // height of footer + bottom padding of email confirmation link padding: 65px 15px; // height of footer + bottom padding of email confirmation link
@media (max-width: $screen-xs-max) {
padding: 0 15px 65px;
}
}
}
// For sign in pane only, to improve tab order, the following removes the submit button from
// normal document flow and pins it to the bottom of the form. For context, see !6867 & !6928
.login-box {
.new_user {
position: relative;
padding-bottom: 35px;
}
.move-submit-down {
position: absolute;
width: 100%;
bottom: 0;
} }
} }
...@@ -183,6 +183,15 @@ ...@@ -183,6 +183,15 @@
.ci-coverage { .ci-coverage {
float: right; float: right;
} }
.stop-env-container {
color: $gl-text-color;
float: right;
a {
color: $gl-text-color;
}
}
} }
.mr_source_commit, .mr_source_commit,
...@@ -429,11 +438,18 @@ ...@@ -429,11 +438,18 @@
} }
} }
.merge-request-tabs { .merge-request-tabs-holder {
background-color: #fff; background-color: #fff;
&.affix { &.affix {
top: 100px; top: 100px;
left: 0;
z-index: 9; z-index: 9;
transition: right .15s;
}
&:not(.affix) .container-fluid {
padding-left: 0;
padding-right: 0;
} }
} }
...@@ -23,10 +23,17 @@ ...@@ -23,10 +23,17 @@
.table.ci-table { .table.ci-table {
min-width: 1200px; min-width: 1200px;
.branch-commit { .pipeline-id {
width: 33%; color: $black;
} }
.branch-commit {
width: 30%;
.branch-name {
max-width: 195px;
}
}
} }
} }
...@@ -73,6 +80,10 @@ ...@@ -73,6 +80,10 @@
border-top-width: 1px; border-top-width: 1px;
} }
.build.retried {
background-color: $gray-lightest;
}
.commit-link { .commit-link {
.ci-status { .ci-status {
...@@ -88,6 +99,15 @@ ...@@ -88,6 +99,15 @@
} }
} }
.avatar {
margin-left: 0;
float: none;
}
.api {
color: $code-color;
}
.branch-commit { .branch-commit {
.branch-name { .branch-name {
...@@ -109,12 +129,11 @@ ...@@ -109,12 +129,11 @@
.fa { .fa {
font-size: 12px; font-size: 12px;
color: $table-text-gray; color: $gl-text-color;
} }
.commit-id { .commit-id {
color: $gl-link-color; color: $gl-link-color;
margin-right: 8px;
} }
.commit-title { .commit-title {
...@@ -125,10 +144,6 @@ ...@@ -125,10 +144,6 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.avatar {
margin-left: 0;
}
.label { .label {
margin-right: 4px; margin-right: 4px;
} }
...@@ -144,17 +159,11 @@ ...@@ -144,17 +159,11 @@
.icon-container { .icon-container {
display: inline-block; display: inline-block;
text-align: right; width: 10px;
width: 15px;
.fa {
position: relative;
right: 3px;
}
svg { &.commit-icon {
position: relative; width: 15px;
right: 1px; text-align: center;
} }
} }
...@@ -360,10 +369,6 @@ ...@@ -360,10 +369,6 @@
&:hover { &:hover {
background-color: $gray-lighter; background-color: $gray-lighter;
.dropdown-menu-toggle {
background-color: transparent;
}
} }
&.playable { &.playable {
...@@ -393,6 +398,15 @@ ...@@ -393,6 +398,15 @@
} }
} }
.tooltip {
white-space: nowrap;
.tooltip-inner {
overflow: hidden;
text-overflow: ellipsis;
}
}
.ci-status-text { .ci-status-text {
width: 135px; width: 135px;
white-space: nowrap; white-space: nowrap;
...@@ -410,6 +424,7 @@ ...@@ -410,6 +424,7 @@
} }
.dropdown-menu-toggle { .dropdown-menu-toggle {
background-color: transparent;
border: none; border: none;
width: auto; width: auto;
padding: 0; padding: 0;
...@@ -634,6 +649,10 @@ ...@@ -634,6 +649,10 @@
&.pipelines { &.pipelines {
.ci-table {
min-width: 900px;
}
.content-list.pipelines { .content-list.pipelines {
overflow: auto; overflow: auto;
} }
......
...@@ -13,9 +13,18 @@ ...@@ -13,9 +13,18 @@
.new_project, .new_project,
.edit-project { .edit-project {
fieldset { fieldset {
&.features .control-label {
font-weight: normal; &.features {
.label-light {
margin-bottom: 0;
}
.help-block {
margin-top: 0;
}
} }
.form-group { .form-group {
...@@ -40,6 +49,7 @@ ...@@ -40,6 +49,7 @@
} }
.input-group > div { .input-group > div {
&:last-child { &:last-child {
padding-right: 0; padding-right: 0;
} }
...@@ -47,6 +57,7 @@ ...@@ -47,6 +57,7 @@
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.input-group > div { .input-group > div {
margin-bottom: 14px; margin-bottom: 14px;
&:last-child { &:last-child {
...@@ -60,6 +71,7 @@ ...@@ -60,6 +71,7 @@
} }
.input-group-addon { .input-group-addon {
&.static-namespace { &.static-namespace {
height: 35px; height: 35px;
border-radius: 3px; border-radius: 3px;
...@@ -761,62 +773,6 @@ pre.light-well { ...@@ -761,62 +773,6 @@ pre.light-well {
.dropdown-menu { .dropdown-menu {
width: 300px; width: 300px;
} }
&.from .compare-dropdown-toggle {
width: 237px;
}
&.to .compare-dropdown-toggle {
width: 254px;
}
.dropdown-toggle-text {
display: block;
height: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
}
.compare-ellipsis {
display: inline;
}
@media (max-width: $screen-xs-max) {
.compare-form-group {
.input-group {
width: 100%;
& > .compare-dropdown-toggle {
width: 100%;
}
}
.dropdown-menu {
width: 100%;
}
}
.compare-switch-container {
text-align: center;
padding: 0 0 $gl-padding;
.commits-compare-switch {
float: none;
}
}
.compare-ellipsis {
display: block;
text-align: center;
padding: 0 0 $gl-padding;
}
.commits-compare-btn {
width: 100%;
}
} }
.clearable-input { .clearable-input {
...@@ -855,3 +811,30 @@ pre.light-well { ...@@ -855,3 +811,30 @@ pre.light-well {
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
} }
} }
.project-home-empty {
border-top: 0;
.container-fluid {
background: none;
}
p {
margin-left: auto;
margin-right: auto;
max-width: 650px;
}
}
.project-feature-nested {
@media (min-width: $screen-sm-min) {
padding-left: 45px;
}
}
.project-repo-select {
&.disabled {
opacity: 0.5;
pointer-events: none;
}
}
...@@ -26,14 +26,10 @@ class Admin::ServicesController < Admin::ApplicationController ...@@ -26,14 +26,10 @@ class Admin::ServicesController < Admin::ApplicationController
private private
def services_templates def services_templates
templates = [] Service.available_services_names.map do |service_name|
Service.available_services_names.each do |service_name|
service_template = service_name.concat("_service").camelize.constantize service_template = service_name.concat("_service").camelize.constantize
templates << service_template.where(template: true).first_or_create service_template.where(template: true).first_or_create
end end
templates
end end
def service def service
......
...@@ -2,6 +2,7 @@ module IssuableActions ...@@ -2,6 +2,7 @@ module IssuableActions
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
before_action :labels, only: [:show, :new, :edit]
before_action :authorize_destroy_issuable!, only: :destroy before_action :authorize_destroy_issuable!, only: :destroy
before_action :authorize_admin_issuable!, only: :bulk_update before_action :authorize_admin_issuable!, only: :bulk_update
end end
...@@ -25,6 +26,10 @@ module IssuableActions ...@@ -25,6 +26,10 @@ module IssuableActions
private private
def labels
@labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
end
def authorize_destroy_issuable! def authorize_destroy_issuable!
unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable) unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable)
return access_denied! return access_denied!
......
class Dashboard::LabelsController < Dashboard::ApplicationController class Dashboard::LabelsController < Dashboard::ApplicationController
def index def index
labels = Label.where(project_id: projects).select(:id, :title, :color).uniq(:title) labels = LabelsFinder.new(current_user).execute
respond_to do |format| respond_to do |format|
format.json { render json: labels } format.json { render json: labels.as_json(only: [:id, :title, :color]) }
end end
end end
end end
class Groups::LabelsController < Groups::ApplicationController
before_action :label, only: [:edit, :update, :destroy]
before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy]
before_action :save_previous_label_path, only: [:edit]
respond_to :html
def index
respond_to do |format|
format.html do
@labels = @group.labels.page(params[:page])
end
format.json do
available_labels = LabelsFinder.new(current_user, group_id: @group.id).execute
render json: available_labels.as_json(only: [:id, :title, :color])
end
end
end
def new
@label = @group.labels.new
@previous_labels_path = previous_labels_path
end
def create
@label = @group.labels.create(label_params)
if @label.valid?
redirect_to group_labels_path(@group)
else
render :new
end
end
def edit
@previous_labels_path = previous_labels_path
end
def update
if @label.update_attributes(label_params)
redirect_back_or_group_labels_path
else
render :edit
end
end
def destroy
@label.destroy
respond_to do |format|
format.html do
redirect_to group_labels_path(@group), notice: 'Label was removed'
end
format.js
end
end
protected
def authorize_admin_labels!
return render_404 unless can?(current_user, :admin_label, @group)
end
def authorize_read_labels!
return render_404 unless can?(current_user, :read_label, @group)
end
def label
@label ||= @group.labels.find(params[:id])
end
def label_params
params.require(:label).permit(:title, :description, :color)
end
def redirect_back_or_group_labels_path(options = {})
redirect_to previous_labels_path, options
end
def previous_labels_path
session.fetch(:previous_labels_path, fallback_path)
end
def fallback_path
group_labels_path(@group)
end
def save_previous_label_path
session[:previous_labels_path] = URI(request.referer || '').path
end
end
...@@ -72,10 +72,10 @@ module Projects ...@@ -72,10 +72,10 @@ module Projects
def serialize_as_json(resource) def serialize_as_json(resource)
resource.as_json( resource.as_json(
labels: true,
only: [:iid, :title, :confidential], only: [:iid, :title, :confidential],
include: { include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, assignee: { only: [:id, :name, :username], methods: [:avatar_url] }
labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
}) })
end end
end end
......
...@@ -76,9 +76,8 @@ module Projects ...@@ -76,9 +76,8 @@ module Projects
resource.as_json( resource.as_json(
only: [:id, :list_type, :position], only: [:id, :list_type, :position],
methods: [:title], methods: [:title],
include: { label: true
label: { only: [:id, :title, :description, :color, :priority] } )
})
end end
end end
end end
......
...@@ -47,7 +47,9 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -47,7 +47,9 @@ class Projects::BuildsController < Projects::ApplicationController
def trace def trace
respond_to do |format| respond_to do |format|
format.json do format.json do
render json: @build.trace_with_state(params[:state].presence).merge!(id: @build.id, status: @build.status) state = params[:state].presence
render json: @build.trace_with_state(state: state).
merge!(id: @build.id, status: @build.status)
end end
end end
end end
......
...@@ -13,7 +13,7 @@ class Projects::CommitsController < Projects::ApplicationController ...@@ -13,7 +13,7 @@ class Projects::CommitsController < Projects::ApplicationController
@commits = @commits =
if search.present? if search.present?
@repository.find_commits_by_message(search, @ref, @path, @limit, @offset).compact @repository.find_commits_by_message(search, @ref, @path, @limit, @offset)
else else
@repository.commits(@ref, path: @path, limit: @limit, offset: @offset) @repository.commits(@ref, path: @path, limit: @limit, offset: @offset)
end end
......
...@@ -2,11 +2,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -2,11 +2,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController
layout 'project' layout 'project'
before_action :authorize_read_environment! before_action :authorize_read_environment!
before_action :authorize_create_environment!, only: [:new, :create] before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_update_environment!, only: [:edit, :update, :destroy] before_action :authorize_create_deployment!, only: [:stop]
before_action :environment, only: [:show, :edit, :update, :destroy] before_action :authorize_update_environment!, only: [:edit, :update]
before_action :environment, only: [:show, :edit, :update, :stop]
def index def index
@environments = project.environments @scope = params[:scope]
@all_environments = project.environments
@environments =
if @scope == 'stopped'
@all_environments.stopped
else
@all_environments.available
end
end end
def show def show
...@@ -38,14 +46,11 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -38,14 +46,11 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end end
end end
def destroy def stop
if @environment.destroy return render_404 unless @environment.stoppable?
flash[:notice] = 'Environment was successfully removed.'
else
flash[:alert] = 'Failed to remove environment.'
end
redirect_to namespace_project_environments_path(project.namespace, project) new_action = @environment.stop!(current_user)
redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action])
end end
private private
......
...@@ -26,7 +26,9 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -26,7 +26,9 @@ class Projects::IssuesController < Projects::ApplicationController
@issues = issues_collection @issues = issues_collection
@issues = @issues.page(params[:page]) @issues = @issues.page(params[:page])
@labels = @project.labels.where(title: params[:label_name]) if params[:label_name].present?
@labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
end
respond_to do |format| respond_to do |format|
format.html format.html
......
...@@ -3,21 +3,22 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -3,21 +3,22 @@ class Projects::LabelsController < Projects::ApplicationController
before_action :module_enabled before_action :module_enabled
before_action :label, only: [:edit, :update, :destroy] before_action :label, only: [:edit, :update, :destroy]
before_action :find_labels, only: [:index, :set_priorities, :remove_priority]
before_action :authorize_read_label! before_action :authorize_read_label!
before_action :authorize_admin_labels!, only: [ before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update,
:new, :create, :edit, :update, :generate, :destroy, :remove_priority, :set_priorities :generate, :destroy, :remove_priority,
] :set_priorities]
respond_to :js, :html respond_to :js, :html
def index def index
@labels = @project.labels.unprioritized.page(params[:page]) @prioritized_labels = @available_labels.prioritized(@project)
@prioritized_labels = @project.labels.prioritized @labels = @available_labels.unprioritized(@project).page(params[:page])
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
render json: @project.labels render json: @available_labels.as_json(only: [:id, :title, :color])
end end
end end
end end
...@@ -36,7 +37,7 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -36,7 +37,7 @@ class Projects::LabelsController < Projects::ApplicationController
end end
else else
respond_to do |format| respond_to do |format|
format.html { render 'new' } format.html { render :new }
format.json { render json: { message: @label.errors.messages }, status: 400 } format.json { render json: { message: @label.errors.messages }, status: 400 }
end end
end end
...@@ -49,7 +50,7 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -49,7 +50,7 @@ class Projects::LabelsController < Projects::ApplicationController
if @label.update_attributes(label_params) if @label.update_attributes(label_params)
redirect_to namespace_project_labels_path(@project.namespace, @project) redirect_to namespace_project_labels_path(@project.namespace, @project)
else else
render 'edit' render :edit
end end
end end
...@@ -68,6 +69,7 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -68,6 +69,7 @@ class Projects::LabelsController < Projects::ApplicationController
def destroy def destroy
@label.destroy @label.destroy
@labels = find_labels
respond_to do |format| respond_to do |format|
format.html do format.html do
...@@ -80,20 +82,24 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -80,20 +82,24 @@ class Projects::LabelsController < Projects::ApplicationController
def remove_priority def remove_priority
respond_to do |format| respond_to do |format|
if label.update_attribute(:priority, nil) label = @available_labels.find(params[:id])
if label.unprioritize!(project)
format.json { render json: label } format.json { render json: label }
else else
message = label.errors.full_messages.uniq.join('. ') format.json { head :unprocessable_entity }
format.json { render json: { message: message }, status: :unprocessable_entity }
end end
end end
end end
def set_priorities def set_priorities
Label.transaction do Label.transaction do
params[:label_ids].each_with_index do |label_id, index| available_labels_ids = @available_labels.where(id: params[:label_ids]).pluck(:id)
label = @project.labels.find_by_id(label_id) label_ids = params[:label_ids].select { |id| available_labels_ids.include?(id.to_i) }
label.update_attribute(:priority, index) if label
label_ids.each_with_index do |label_id, index|
label = @available_labels.find(label_id)
label.prioritize!(project, index)
end end
end end
...@@ -119,6 +125,10 @@ class Projects::LabelsController < Projects::ApplicationController ...@@ -119,6 +125,10 @@ class Projects::LabelsController < Projects::ApplicationController
end end
alias_method :subscribable_resource, :label alias_method :subscribable_resource, :label
def find_labels
@available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute.includes(:priorities)
end
def authorize_admin_labels! def authorize_admin_labels!
return render_404 unless can?(current_user, :admin_label, @project) return render_404 unless can?(current_user, :admin_label, @project)
end end
......
...@@ -40,7 +40,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -40,7 +40,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_requests = @merge_requests.page(params[:page]) @merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(:target_project) @merge_requests = @merge_requests.preload(:target_project)
@labels = @project.labels.where(title: params[:label_name]) if params[:label_name].present?
labels_params = { project_id: @project.id, title: params[:label_name] }
@labels = LabelsFinder.new(current_user, labels_params).execute
end
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -395,7 +398,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -395,7 +398,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
status ||= "preparing" status ||= "preparing"
else else
ci_service = @merge_request.source_project.ci_service ci_service = @merge_request.source_project.try(:ci_service)
status = ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service status = ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service
if ci_service.respond_to?(:commit_coverage) if ci_service.respond_to?(:commit_coverage)
...@@ -422,10 +425,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -422,10 +425,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController
project = environment.project project = environment.project
deployment = environment.first_deployment_for(@merge_request.diff_head_commit) deployment = environment.first_deployment_for(@merge_request.diff_head_commit)
stop_url =
if environment.stoppable? && can?(current_user, :create_deployment, environment)
stop_namespace_project_environment_path(project.namespace, project, environment)
end
{ {
id: environment.id, id: environment.id,
name: environment.name, name: environment.name,
url: namespace_project_environment_path(project.namespace, project, environment), url: namespace_project_environment_path(project.namespace, project, environment),
stop_url: stop_url,
external_url: environment.external_url, external_url: environment.external_url,
external_url_formatted: environment.formatted_external_url, external_url_formatted: environment.formatted_external_url,
deployed_at: deployment.try(:created_at), deployed_at: deployment.try(:created_at),
...@@ -483,13 +492,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -483,13 +492,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@noteable = @merge_request @noteable = @merge_request
@commits_count = @merge_request.commits.count @commits_count = @merge_request.commits.count
@pipeline = @merge_request.pipeline
@statuses = @pipeline.statuses.relevant if @pipeline
if @merge_request.locked_long_ago? if @merge_request.locked_long_ago?
@merge_request.unlock_mr @merge_request.unlock_mr
@merge_request.close @merge_request.close
end end
define_pipelines_vars
end end
# Discussion tab data is rendered on html responses of actions # Discussion tab data is rendered on html responses of actions
...@@ -517,7 +525,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -517,7 +525,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_widget_vars def define_widget_vars
@pipeline = @merge_request.pipeline @pipeline = @merge_request.pipeline
@pipelines = [@pipeline].compact
end end
def define_commit_vars def define_commit_vars
...@@ -544,6 +551,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -544,6 +551,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController
) )
end end
def define_pipelines_vars
@pipelines = @merge_request.all_pipelines
if @pipelines.present?
@pipeline = @pipelines.first
@statuses = @pipeline.statuses.relevant
end
end
def define_new_vars def define_new_vars
@noteable = @merge_request @noteable = @merge_request
...@@ -559,10 +575,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -559,10 +575,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@commit = @merge_request.diff_head_commit @commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit @base_commit = @merge_request.diff_base_commit
@pipeline = @merge_request.pipeline
@statuses = @pipeline.statuses.relevant if @pipeline
@note_counts = Note.where(commit_id: @commits.map(&:id)). @note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count group(:commit_id).count
@labels = LabelsFinder.new(current_user, project_id: @project.id).execute
define_pipelines_vars
end end
def invalid_mr def invalid_mr
......
...@@ -32,21 +32,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -32,21 +32,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
current_user: current_user current_user: current_user
) )
if params[:group_ids].present?
group_ids = params[:group_ids].split(',')
groups = Group.where(id: group_ids)
groups.each do |group|
next unless can?(current_user, :read_group, group)
project.project_group_links.create(
group: group,
group_access: params[:access_level],
expires_at: params[:expires_at]
)
end
end
redirect_to namespace_project_project_members_path(@project.namespace, @project) redirect_to namespace_project_project_members_path(@project.namespace, @project)
end end
......
class ProjectsController < Projects::ApplicationController class ProjectsController < Projects::ApplicationController
include IssuableCollections
include ExtractsPath include ExtractsPath
before_action :authenticate_user!, except: [:show, :activity, :refs] before_action :authenticate_user!, except: [:show, :activity, :refs]
...@@ -103,16 +104,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -103,16 +104,7 @@ class ProjectsController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
@notification_setting = current_user.notification_settings_for(@project) if current_user @notification_setting = current_user.notification_settings_for(@project) if current_user
render_landing_page
if @project.repository_exists?
if @project.empty_repo?
render 'projects/empty'
else
render :show
end
else
render 'projects/no_repo'
end
end end
format.atom do format.atom do
...@@ -285,6 +277,26 @@ class ProjectsController < Projects::ApplicationController ...@@ -285,6 +277,26 @@ class ProjectsController < Projects::ApplicationController
private private
# Render project landing depending of which features are available
# So if page is not availble in the list it renders the next page
#
# pages list order: repository readme, wiki home, issues list, customize workflow
def render_landing_page
if @project.feature_available?(:repository, current_user)
return render 'projects/no_repo' unless @project.repository_exists?
render 'projects/empty' if @project.empty_repo?
else
if @project.wiki_enabled?
@wiki_home = @project.wiki.find_page('home', params[:version_id])
elsif @project.feature_available?(:issues, current_user)
@issues = issues_collection
@issues = @issues.page(params[:page])
end
render :show
end
end
def determine_layout def determine_layout
if [:new, :create].include?(action_name.to_sym) if [:new, :create].include?(action_name.to_sym)
'application' 'application'
...@@ -308,7 +320,8 @@ class ProjectsController < Projects::ApplicationController ...@@ -308,7 +320,8 @@ class ProjectsController < Projects::ApplicationController
project_feature_attributes: project_feature_attributes:
[ [
:issues_access_level, :builds_access_level, :issues_access_level, :builds_access_level,
:wiki_access_level, :merge_requests_access_level, :snippets_access_level :wiki_access_level, :merge_requests_access_level,
:snippets_access_level, :repository_access_level
] ]
} }
......
...@@ -124,14 +124,11 @@ class IssuableFinder ...@@ -124,14 +124,11 @@ class IssuableFinder
def labels def labels
return @labels if defined?(@labels) return @labels if defined?(@labels)
@labels =
if labels? && !filter_by_no_label? if labels? && !filter_by_no_label?
@labels = Label.where(title: label_names) LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute
if projects
@labels = @labels.where(project: projects)
end
else else
@labels = Label.none Label.none
end end
end end
...@@ -274,8 +271,10 @@ class IssuableFinder ...@@ -274,8 +271,10 @@ class IssuableFinder
items = items.without_label items = items.without_label
else else
items = items.with_label(label_names, params[:sort]) items = items.with_label(label_names, params[:sort])
if projects if projects
items = items.where(labels: { project_id: projects }) label_ids = LabelsFinder.new(current_user, project_ids: projects).execute.select(:id)
items = items.where(labels: { id: label_ids })
end end
end end
end end
......
class LabelsFinder < UnionFinder
def initialize(current_user, params = {})
@current_user = current_user
@params = params
end
def execute(authorized_only: true)
@authorized_only = authorized_only
items = find_union(label_ids, Label)
items = with_title(items)
sort(items)
end
private
attr_reader :current_user, :params, :authorized_only
def label_ids
label_ids = []
if project
label_ids << project.group.labels if project.group.present?
label_ids << project.labels
else
label_ids << Label.where(group_id: projects.group_ids)
label_ids << Label.where(project_id: projects.select(:id))
end
label_ids
end
def sort(items)
items.reorder(title: :asc)
end
def with_title(items)
items = items.where(title: title) if title
items
end
def group_id
params[:group_id].presence
end
def project_id
params[:project_id].presence
end
def projects_ids
params[:project_ids].presence
end
def title
params[:title].presence || params[:name].presence
end
def project
return @project if defined?(@project)
if project_id
@project = find_project
else
@project = nil
end
@project
end
def find_project
if authorized_only
available_projects.find_by(id: project_id)
else
Project.find_by(id: project_id)
end
end
def projects
return @projects if defined?(@projects)
@projects = authorized_only ? available_projects : Project.all
@projects = @projects.in_namespace(group_id) if group_id
@projects = @projects.where(id: projects_ids) if projects_ids
@projects = @projects.reorder(nil)
@projects
end
def available_projects
@available_projects ||= ProjectsFinder.new.execute(current_user)
end
end
module AwardEmojiHelper module AwardEmojiHelper
def toggle_award_url(awardable) def toggle_award_url(awardable)
if @project return url_for([:toggle_award_emoji, awardable]) unless @project
url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
if awardable.is_a?(Note)
# We render a list of notes very frequently and calling the specific method is a lot faster than the generic one (4.5x)
toggle_award_emoji_namespace_project_note_url(@project.namespace, @project, awardable.id)
else else
url_for([:toggle_award_emoji, awardable]) url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
end end
end end
end end
module BuildsHelper
def sidebar_build_class(build, current_build)
build_class = ''
build_class += ' active' if build == current_build
build_class += ' retried' if build.retried?
build_class
end
end
...@@ -154,7 +154,7 @@ module EventsHelper ...@@ -154,7 +154,7 @@ module EventsHelper
end end
def event_commit_title(message) def event_commit_title(message)
escape_once(truncate(message.split("\n").first, length: 70)) (message.split("\n").first || "").truncate(70)
rescue rescue
"--broken encoding" "--broken encoding"
end end
......
...@@ -94,6 +94,22 @@ module GitlabRoutingHelper ...@@ -94,6 +94,22 @@ module GitlabRoutingHelper
namespace_project_merge_request_url(entity.project.namespace, entity.project, entity, *args) namespace_project_merge_request_url(entity.project.namespace, entity.project, entity, *args)
end end
def pipeline_url(pipeline, *args)
namespace_project_pipeline_url(pipeline.project.namespace, pipeline.project, pipeline.id, *args)
end
def pipeline_build_url(pipeline, build, *args)
namespace_project_build_url(pipeline.project.namespace, pipeline.project, build.id, *args)
end
def commits_url(entity, *args)
namespace_project_commits_url(entity.project.namespace, entity.project, entity.ref, *args)
end
def commit_url(entity, *args)
namespace_project_commit_url(entity.project.namespace, entity.project, entity.sha, *args)
end
def project_snippet_url(entity, *args) def project_snippet_url(entity, *args)
namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args) namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args)
end end
......
...@@ -124,6 +124,10 @@ module IssuablesHelper ...@@ -124,6 +124,10 @@ module IssuablesHelper
end end
end end
def issuable_filters_present
params[:search] || params[:author_id] || params[:assignee_id] || params[:milestone_title] || params[:label_name]
end
def issuables_count_for_state(issuable_type, state) def issuables_count_for_state(issuable_type, state)
issuables_finder = public_send("#{issuable_type}_finder") issuables_finder = public_send("#{issuable_type}_finder")
issuables_finder.params[:state] = state issuables_finder.params[:state] = state
......
...@@ -4,9 +4,8 @@ module LabelsHelper ...@@ -4,9 +4,8 @@ module LabelsHelper
# Link to a Label # Link to a Label
# #
# label - Label object to link to # label - Label object to link to
# project - Project object which will be used as the context for the label's # subject - Project/Group object which will be used as the context for the
# link. If omitted, defaults to `@project`, or the label's own # label's link. If omitted, defaults to the label's own group/project.
# project.
# type - The type of item the link will point to (:issue or # type - The type of item the link will point to (:issue or
# :merge_request). If omitted, defaults to :issue. # :merge_request). If omitted, defaults to :issue.
# block - An optional block that will be passed to `link_to`, forming the # block - An optional block that will be passed to `link_to`, forming the
...@@ -15,15 +14,14 @@ module LabelsHelper ...@@ -15,15 +14,14 @@ module LabelsHelper
# #
# Examples: # Examples:
# #
# # Allow the generated link to use the label's own project # # Allow the generated link to use the label's own subject
# link_to_label(label) # link_to_label(label)
# #
# # Force the generated link to use @project # # Force the generated link to use a provided group
# @project = Project.first # link_to_label(label, subject: Group.last)
# link_to_label(label)
# #
# # Force the generated link to use a provided project # # Force the generated link to use a provided project
# link_to_label(label, project: Project.last) # link_to_label(label, subject: Project.last)
# #
# # Force the generated link to point to merge requests instead of issues # # Force the generated link to point to merge requests instead of issues
# link_to_label(label, type: :merge_request) # link_to_label(label, type: :merge_request)
...@@ -32,9 +30,8 @@ module LabelsHelper ...@@ -32,9 +30,8 @@ module LabelsHelper
# link_to_label(label) { "My Custom Label Text" } # link_to_label(label) { "My Custom Label Text" }
# #
# Returns a String # Returns a String
def link_to_label(label, project: nil, type: :issue, tooltip: true, css_class: nil, &block) def link_to_label(label, subject: nil, type: :issue, tooltip: true, css_class: nil, &block)
project ||= @project || label.project link = label_filter_path(subject || label.subject, label, type: type)
link = label_filter_path(project, label, type: type)
if block_given? if block_given?
link_to link, class: css_class, &block link_to link, class: css_class, &block
...@@ -43,15 +40,40 @@ module LabelsHelper ...@@ -43,15 +40,40 @@ module LabelsHelper
end end
end end
def label_filter_path(project, label, type: issue) def label_filter_path(subject, label, type: :issue)
case subject
when Group
send("#{type.to_s.pluralize}_group_path",
subject,
label_name: [label.name])
when Project
send("namespace_project_#{type.to_s.pluralize}_path", send("namespace_project_#{type.to_s.pluralize}_path",
project.namespace, subject.namespace,
project, subject,
label_name: [label.name]) label_name: [label.name])
end end
end
def project_label_names def edit_label_path(label)
@project.labels.pluck(:title) case label
when GroupLabel then edit_group_label_path(label.group, label)
when ProjectLabel then edit_namespace_project_label_path(label.project.namespace, label.project, label)
end
end
def destroy_label_path(label)
case label
when GroupLabel then group_label_path(label.group, label)
when ProjectLabel then namespace_project_label_path(label.project.namespace, label.project, label)
end
end
def toggle_subscription_data(label)
return unless label.is_a?(ProjectLabel)
{
url: toggle_subscription_namespace_project_label_path(label.project.namespace, label.project, label)
}
end end
def render_colored_label(label, label_suffix = '', tooltip: true) def render_colored_label(label, label_suffix = '', tooltip: true)
...@@ -68,8 +90,8 @@ module LabelsHelper ...@@ -68,8 +90,8 @@ module LabelsHelper
span.html_safe span.html_safe
end end
def render_colored_cross_project_label(label, tooltip: true) def render_colored_cross_project_label(label, source_project = nil, tooltip: true)
label_suffix = label.project.name_with_namespace label_suffix = source_project ? source_project.name_with_namespace : label.project.name_with_namespace
label_suffix = " <i>in #{escape_once(label_suffix)}</i>" label_suffix = " <i>in #{escape_once(label_suffix)}</i>"
render_colored_label(label, label_suffix, tooltip: tooltip) render_colored_label(label, label_suffix, tooltip: tooltip)
end end
...@@ -115,7 +137,10 @@ module LabelsHelper ...@@ -115,7 +137,10 @@ module LabelsHelper
end end
def labels_filter_path def labels_filter_path
return group_labels_path(@group, :json) if @group
project = @target_project || @project project = @target_project || @project
if project if project
namespace_project_labels_path(project.namespace, project, :json) namespace_project_labels_path(project.namespace, project, :json)
else else
...@@ -124,11 +149,24 @@ module LabelsHelper ...@@ -124,11 +149,24 @@ module LabelsHelper
end end
def label_subscription_status(label) def label_subscription_status(label)
label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed' case label
when GroupLabel then 'Subscribing to group labels is currently not supported.'
when ProjectLabel then label.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
end
end end
def label_subscription_toggle_button_text(label) def label_subscription_toggle_button_text(label)
label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe' case label
when GroupLabel then 'Subscribing to group labels is currently not supported.'
when ProjectLabel then label.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
end
end
def label_deletion_confirm_text(label)
case label
when GroupLabel then 'Remove this label? This will affect all projects within the group. Are you sure?'
when ProjectLabel then 'Remove this label? Are you sure?'
end
end end
# Required for Banzai::Filter::LabelReferenceFilter # Required for Banzai::Filter::LabelReferenceFilter
......
...@@ -86,11 +86,15 @@ module MergeRequestsHelper ...@@ -86,11 +86,15 @@ module MergeRequestsHelper
end end
def source_branch_with_namespace(merge_request) def source_branch_with_namespace(merge_request)
branch = link_to(merge_request.source_branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch)) namespace = merge_request.source_project_namespace
branch = merge_request.source_branch
if merge_request.source_branch_exists?
namespace = link_to(namespace, project_path(merge_request.source_project))
branch = link_to(branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch))
end
if merge_request.for_fork? if merge_request.for_fork?
namespace = link_to(merge_request.source_project_namespace,
project_path(merge_request.source_project))
namespace + ":" + branch namespace + ":" + branch
else else
branch branch
......
...@@ -50,6 +50,20 @@ module PreferencesHelper ...@@ -50,6 +50,20 @@ module PreferencesHelper
end end
def default_project_view def default_project_view
current_user ? current_user.project_view : 'readme' return 'readme' unless current_user
user_view = current_user.project_view
if @project.feature_available?(:repository, current_user)
user_view
elsif user_view == "activity"
"activity"
elsif @project.wiki_enabled?
"wiki"
elsif @project.feature_available?(:issues, current_user)
"projects/issues/issues"
else
"customize_workflow"
end
end end
end end
...@@ -134,16 +134,35 @@ module ProjectsHelper ...@@ -134,16 +134,35 @@ module ProjectsHelper
options = project_feature_options options = project_feature_options
if @project.private? if @project.private?
level = @project.project_feature.send(field)
options.delete('Everyone with access') options.delete('Everyone with access')
highest_available_option = options.values.max if @project.project_feature.send(field) == ProjectFeature::ENABLED highest_available_option = options.values.max if level == ProjectFeature::ENABLED
end end
options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field)) options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field))
content_tag(:select, options, name: "project[project_feature_attributes][#{field}]", id: "project_project_feature_attributes_#{field}", class: "pull-right form-control", data: { field: field }).html_safe
content_tag(
:select,
options,
name: "project[project_feature_attributes][#{field}]",
id: "project_project_feature_attributes_#{field}",
class: "pull-right form-control #{repo_children_classes(field)}",
data: { field: field }
).html_safe
end end
private private
def repo_children_classes(field)
needs_repo_check = [:merge_requests_access_level, :builds_access_level]
return unless needs_repo_check.include?(field)
classes = "project-repo-select js-repo-select"
classes << " disabled" unless @project.feature_available?(:repository, current_user)
classes
end
def get_project_nav_tabs(project, current_user) def get_project_nav_tabs(project, current_user)
nav_tabs = [:home] nav_tabs = [:home]
...@@ -155,12 +174,8 @@ module ProjectsHelper ...@@ -155,12 +174,8 @@ module ProjectsHelper
nav_tabs << :merge_requests nav_tabs << :merge_requests
end end
if can?(current_user, :read_pipeline, project)
nav_tabs << :pipelines
end
if can?(current_user, :read_build, project) if can?(current_user, :read_build, project)
nav_tabs << :builds nav_tabs << :pipelines
end end
if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project) if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project)
...@@ -435,4 +450,8 @@ module ProjectsHelper ...@@ -435,4 +450,8 @@ module ProjectsHelper
'Everyone with access' => ProjectFeature::ENABLED 'Everyone with access' => ProjectFeature::ENABLED
} }
end end
def project_child_container_class(view_path)
view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}"
end
end end
module Emails
module Pipelines
def pipeline_success_email(pipeline, to)
pipeline_mail(pipeline, to, 'succeeded')
end
def pipeline_failed_email(pipeline, to)
pipeline_mail(pipeline, to, 'failed')
end
private
def pipeline_mail(pipeline, to, status)
@project = pipeline.project
@pipeline = pipeline
@merge_request = pipeline.merge_requests.first
add_headers
mail(to: to, subject: pipeline_subject(status), skip_premailer: true) do |format|
format.html { render layout: false }
format.text
end
end
def add_headers
add_project_headers
add_pipeline_headers
end
def add_pipeline_headers
headers['X-GitLab-Pipeline-Id'] = @pipeline.id
headers['X-GitLab-Pipeline-Ref'] = @pipeline.ref
headers['X-GitLab-Pipeline-Status'] = @pipeline.status
end
def pipeline_subject(status)
commit = @pipeline.short_sha
commit << " in #{@merge_request.to_reference}" if @merge_request
subject("Pipeline ##{@pipeline.id} has #{status} for #{@pipeline.ref}", commit)
end
end
end
...@@ -7,6 +7,7 @@ class Notify < BaseMailer ...@@ -7,6 +7,7 @@ class Notify < BaseMailer
include Emails::Projects include Emails::Projects
include Emails::Profile include Emails::Profile
include Emails::Builds include Emails::Builds
include Emails::Pipelines
include Emails::Members include Emails::Members
add_template_helper MergeRequestsHelper add_template_helper MergeRequestsHelper
......
...@@ -133,13 +133,17 @@ module Ci ...@@ -133,13 +133,17 @@ module Ci
latest_builds.where('stage_idx < ?', stage_idx) latest_builds.where('stage_idx < ?', stage_idx)
end end
def trace_html def trace_html(**args)
trace_with_state[:html] || '' trace_with_state(**args)[:html] || ''
end end
def trace_with_state(state = nil) def trace_with_state(state: nil, last_lines: nil)
trace_with_state = Ci::Ansi2html::convert(trace, state) if trace.present? trace_ansi = trace(last_lines: last_lines)
trace_with_state || {} if trace_ansi.present?
Ci::Ansi2html.convert(trace_ansi, state)
else
{}
end
end end
def timeout def timeout
...@@ -222,9 +226,10 @@ module Ci ...@@ -222,9 +226,10 @@ module Ci
raw_trace.present? raw_trace.present?
end end
def raw_trace def raw_trace(last_lines: nil)
if File.exist?(trace_file_path) if File.exist?(trace_file_path)
File.read(trace_file_path) Gitlab::Ci::TraceReader.new(trace_file_path).
read(last_lines: last_lines)
else else
# backward compatibility # backward compatibility
read_attribute :trace read_attribute :trace
...@@ -239,8 +244,8 @@ module Ci ...@@ -239,8 +244,8 @@ module Ci
project.ci_id && File.exist?(old_path_to_trace) project.ci_id && File.exist?(old_path_to_trace)
end end
def trace def trace(last_lines: nil)
hide_secrets(raw_trace) hide_secrets(raw_trace(last_lines: last_lines))
end end
def trace_length def trace_length
......
...@@ -19,7 +19,7 @@ module Ci ...@@ -19,7 +19,7 @@ module Ci
validates_presence_of :status, unless: :importing? validates_presence_of :status, unless: :importing?
validate :valid_commit_sha, unless: :importing? validate :valid_commit_sha, unless: :importing?
after_save :keep_around_commits, unless: :importing? after_create :keep_around_commits, unless: :importing?
delegate :stages, to: :statuses delegate :stages, to: :statuses
...@@ -59,9 +59,6 @@ module Ci ...@@ -59,9 +59,6 @@ module Ci
before_transition any => [:success, :failed, :canceled] do |pipeline| before_transition any => [:success, :failed, :canceled] do |pipeline|
pipeline.finished_at = Time.now pipeline.finished_at = Time.now
end
before_transition do |pipeline|
pipeline.update_duration pipeline.update_duration
end end
...@@ -154,7 +151,7 @@ module Ci ...@@ -154,7 +151,7 @@ module Ci
def retryable? def retryable?
builds.latest.any? do |build| builds.latest.any? do |build|
build.failed? && build.retryable? (build.failed? || build.canceled?) && build.retryable?
end end
end end
......
...@@ -5,11 +5,15 @@ module Expirable ...@@ -5,11 +5,15 @@ module Expirable
scope :expired, -> { where('expires_at <= ?', Time.current) } scope :expired, -> { where('expires_at <= ?', Time.current) }
end end
def expired?
expires? && expires_at <= Time.current
end
def expires? def expires?
expires_at.present? expires_at.present?
end end
def expires_soon? def expires_soon?
expires_at < 7.days.from_now expires? && expires_at < 7.days.from_now
end end
end end
...@@ -5,6 +5,7 @@ module HasStatus ...@@ -5,6 +5,7 @@ module HasStatus
STARTED_STATUSES = %w[running success failed skipped] STARTED_STATUSES = %w[running success failed skipped]
ACTIVE_STATUSES = %w[pending running] ACTIVE_STATUSES = %w[pending running]
COMPLETED_STATUSES = %w[success failed canceled] COMPLETED_STATUSES = %w[success failed canceled]
ORDERED_STATUSES = %w[failed pending running canceled success skipped]
class_methods do class_methods do
def status_sql def status_sql
......
...@@ -145,8 +145,14 @@ module Issuable ...@@ -145,8 +145,14 @@ module Issuable
end end
def order_labels_priority(excluded_labels: []) def order_labels_priority(excluded_labels: [])
condition_field = "#{table_name}.id" params = {
highest_priority = highest_label_priority(name, condition_field, excluded_labels: excluded_labels).to_sql target_type: name,
target_column: "#{table_name}.id",
project_column: "#{table_name}.#{project_foreign_key}",
excluded_labels: excluded_labels
}
highest_priority = highest_label_priority(params).to_sql
select("#{table_name}.*, (#{highest_priority}) AS highest_priority"). select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
group(arel_table[:id]). group(arel_table[:id]).
...@@ -230,18 +236,6 @@ module Issuable ...@@ -230,18 +236,6 @@ module Issuable
labels.order('title ASC').pluck(:title) labels.order('title ASC').pluck(:title)
end end
def remove_labels
labels.delete_all
end
def add_labels_by_names(label_names)
label_names.each do |label_name|
label = project.labels.create_with(color: Label::DEFAULT_COLOR).
find_or_create_by(title: label_name.strip)
self.labels << label
end
end
# Convert this Issuable class name to a format usable by Ability definitions # Convert this Issuable class name to a format usable by Ability definitions
# #
# Examples: # Examples:
......
...@@ -38,11 +38,13 @@ module Sortable ...@@ -38,11 +38,13 @@ module Sortable
private private
def highest_label_priority(object_types, condition_field, excluded_labels: []) def highest_label_priority(target_type:, target_column:, project_column:, excluded_labels: [])
query = Label.select(Label.arel_table[:priority].minimum). query = Label.select(LabelPriority.arel_table[:priority].minimum).
left_join_priorities.
joins(:label_links). joins(:label_links).
where(label_links: { target_type: object_types }). where("label_priorities.project_id = #{project_column}").
where("label_links.target_id = #{condition_field}"). where(label_links: { target_type: target_type }).
where("label_links.target_id = #{target_column}").
reorder(nil) reorder(nil)
query.where.not(title: excluded_labels) if excluded_labels.present? query.where.not(title: excluded_labels) if excluded_labels.present?
......
...@@ -11,7 +11,7 @@ class Deployment < ActiveRecord::Base ...@@ -11,7 +11,7 @@ class Deployment < ActiveRecord::Base
delegate :name, to: :environment, prefix: true delegate :name, to: :environment, prefix: true
after_save :create_ref after_create :create_ref
def commit def commit
project.commit(sha) project.commit(sha)
...@@ -34,7 +34,7 @@ class Deployment < ActiveRecord::Base ...@@ -34,7 +34,7 @@ class Deployment < ActiveRecord::Base
end end
def manual_actions def manual_actions
deployable.try(:other_actions) @manual_actions ||= deployable.try(:other_actions)
end end
def includes_commit?(commit) def includes_commit?(commit)
...@@ -84,6 +84,17 @@ class Deployment < ActiveRecord::Base ...@@ -84,6 +84,17 @@ class Deployment < ActiveRecord::Base
take take
end end
def stop_action
return nil unless on_stop.present?
return nil unless manual_actions
@stop_action ||= manual_actions.find_by(name: on_stop)
end
def stoppable?
stop_action.present?
end
def formatted_deployment_time def formatted_deployment_time
created_at.to_time.in_time_zone.to_s(:medium) created_at.to_time.in_time_zone.to_s(:medium)
end end
...@@ -91,6 +102,6 @@ class Deployment < ActiveRecord::Base ...@@ -91,6 +102,6 @@ class Deployment < ActiveRecord::Base
private private
def ref_path def ref_path
File.join(environment.ref_path, 'deployments', id.to_s) File.join(environment.ref_path, 'deployments', iid.to_s)
end end
end end
...@@ -7,10 +7,8 @@ class Email < ActiveRecord::Base ...@@ -7,10 +7,8 @@ class Email < ActiveRecord::Base
validates :email, presence: true, uniqueness: true, email: true validates :email, presence: true, uniqueness: true, email: true
validate :unique_email, if: ->(email) { email.email_changed? } validate :unique_email, if: ->(email) { email.email_changed? }
before_validation :cleanup_email def email=(value)
write_attribute(:email, value.downcase.strip)
def cleanup_email
self.email = self.email.downcase.strip
end end
def unique_email def unique_email
......
...@@ -19,6 +19,24 @@ class Environment < ActiveRecord::Base ...@@ -19,6 +19,24 @@ class Environment < ActiveRecord::Base
allow_nil: true, allow_nil: true,
addressable_url: true addressable_url: true
delegate :stop_action, to: :last_deployment, allow_nil: true
scope :available, -> { with_state(:available) }
scope :stopped, -> { with_state(:stopped) }
state_machine :state, initial: :available do
event :start do
transition stopped: :available
end
event :stop do
transition available: :stopped
end
state :available
state :stopped
end
def last_deployment def last_deployment
deployments.last deployments.last
end end
...@@ -53,8 +71,8 @@ class Environment < ActiveRecord::Base ...@@ -53,8 +71,8 @@ class Environment < ActiveRecord::Base
return nil unless ref return nil unless ref
deployment_id = ref.split('/').last deployment_iid = ref.split('/').last
deployments.find(deployment_id) deployments.find_by(iid: deployment_iid)
end end
def ref_path def ref_path
...@@ -66,4 +84,14 @@ class Environment < ActiveRecord::Base ...@@ -66,4 +84,14 @@ class Environment < ActiveRecord::Base
external_url.gsub(/\A.*?:\/\//, '') external_url.gsub(/\A.*?:\/\//, '')
end end
def stoppable?
available? && stop_action.present?
end
def stop!(current_user)
return unless stoppable?
stop_action.play(current_user)
end
end end
...@@ -12,6 +12,7 @@ class Event < ActiveRecord::Base ...@@ -12,6 +12,7 @@ class Event < ActiveRecord::Base
JOINED = 8 # User joined project JOINED = 8 # User joined project
LEFT = 9 # User left project LEFT = 9 # User left project
DESTROYED = 10 DESTROYED = 10
EXPIRED = 11 # User left project due to expiry
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
...@@ -115,6 +116,10 @@ class Event < ActiveRecord::Base ...@@ -115,6 +116,10 @@ class Event < ActiveRecord::Base
action == LEFT action == LEFT
end end
def expired?
action == EXPIRED
end
def destroyed? def destroyed?
action == DESTROYED action == DESTROYED
end end
...@@ -124,7 +129,7 @@ class Event < ActiveRecord::Base ...@@ -124,7 +129,7 @@ class Event < ActiveRecord::Base
end end
def membership_changed? def membership_changed?
joined? || left? joined? || left? || expired?
end end
def created_project? def created_project?
...@@ -184,6 +189,8 @@ class Event < ActiveRecord::Base ...@@ -184,6 +189,8 @@ class Event < ActiveRecord::Base
'joined' 'joined'
elsif left? elsif left?
'left' 'left'
elsif expired?
'removed due to membership expiration from'
elsif destroyed? elsif destroyed?
'destroyed' 'destroyed'
elsif commented? elsif commented?
......
...@@ -29,11 +29,6 @@ class ExternalIssue ...@@ -29,11 +29,6 @@ class ExternalIssue
@project @project
end end
# Pattern used to extract `JIRA-123` issue references from text
def self.reference_pattern
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
end
def to_reference(_from_project = nil) def to_reference(_from_project = nil)
id id
end end
......
...@@ -19,6 +19,7 @@ class Group < Namespace ...@@ -19,6 +19,7 @@ class Group < Namespace
has_many :project_group_links, dependent: :destroy has_many :project_group_links, dependent: :destroy
has_many :shared_projects, through: :project_group_links, source: :project has_many :shared_projects, through: :project_group_links, source: :project
has_many :notification_settings, dependent: :destroy, as: :source has_many :notification_settings, dependent: :destroy, as: :source
has_many :labels, class_name: 'GroupLabel'
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validate :visibility_level_allowed_by_projects validate :visibility_level_allowed_by_projects
...@@ -67,7 +68,7 @@ class Group < Namespace ...@@ -67,7 +68,7 @@ class Group < Namespace
end end
def web_url def web_url
Gitlab::Routing.url_helpers.group_url(self) Gitlab::Routing.url_helpers.group_canonical_url(self)
end end
def human_name def human_name
......
class GroupLabel < Label
belongs_to :group
validates :group, presence: true
alias_attribute :subject, :group
def to_reference(source_project = nil, target_project = nil, format: :id)
super(source_project, target_project, format: format)
end
end
...@@ -138,6 +138,10 @@ class Issue < ActiveRecord::Base ...@@ -138,6 +138,10 @@ class Issue < ActiveRecord::Base
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end end
def self.project_foreign_key
'project_id'
end
def self.sort(method, excluded_labels: []) def self.sort(method, excluded_labels: [])
case method.to_s case method.to_s
when 'due_date_asc' then order_due_date_asc when 'due_date_asc' then order_due_date_asc
...@@ -207,7 +211,13 @@ class Issue < ActiveRecord::Base ...@@ -207,7 +211,13 @@ class Issue < ActiveRecord::Base
note.all_references(current_user, extractor: ext) note.all_references(current_user, extractor: ext)
end end
ext.merge_requests.select { |mr| mr.open? && mr.closes_issue?(self) } merge_requests = ext.merge_requests.select(&:open?)
if merge_requests.any?
ids = MergeRequestsClosingIssues.where(merge_request_id: merge_requests.map(&:id), issue_id: id).pluck(:merge_request_id)
merge_requests.select { |mr| mr.id.in?(ids) }
else
[]
end
end end
def moved? def moved?
...@@ -274,4 +284,16 @@ class Issue < ActiveRecord::Base ...@@ -274,4 +284,16 @@ class Issue < ActiveRecord::Base
def check_for_spam? def check_for_spam?
project.public? project.public?
end end
def as_json(options = {})
super(options).tap do |json|
if options.has_key?(:labels)
json[:labels] = labels.as_json(
project: project,
only: [:id, :title, :description, :color],
methods: [:text_color]
)
end
end
end
end end
...@@ -15,34 +15,49 @@ class Label < ActiveRecord::Base ...@@ -15,34 +15,49 @@ class Label < ActiveRecord::Base
default_value_for :color, DEFAULT_COLOR default_value_for :color, DEFAULT_COLOR
belongs_to :project
has_many :lists, dependent: :destroy has_many :lists, dependent: :destroy
has_many :priorities, class_name: 'LabelPriority'
has_many :label_links, dependent: :destroy has_many :label_links, dependent: :destroy
has_many :issues, through: :label_links, source: :target, source_type: 'Issue' has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest' has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
validates :color, color: true, allow_blank: false validates :color, color: true, allow_blank: false
validates :project, presence: true, unless: Proc.new { |service| service.template? }
# Don't allow ',' for label titles # Don't allow ',' for label titles
validates :title, validates :title, presence: true, format: { with: /\A[^,]+\z/ }
presence: true, validates :title, uniqueness: { scope: [:group_id, :project_id] }
format: { with: /\A[^,]+\z/ },
uniqueness: { scope: :project_id }
before_save :nullify_priority
default_scope { order(title: :asc) } default_scope { order(title: :asc) }
scope :templates, -> { where(template: true) } scope :templates, -> { where(template: true) }
scope :with_title, ->(title) { where(title: title) }
def self.prioritized def self.prioritized(project)
where.not(priority: nil).reorder(:priority, :title) joins(:priorities)
.where(label_priorities: { project_id: project })
.reorder('label_priorities.priority ASC, labels.title ASC')
end end
def self.unprioritized def self.unprioritized(project)
where(priority: nil) labels = Label.arel_table
priorities = LabelPriority.arel_table
label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin).
on(labels[:id].eq(priorities[:label_id]).and(priorities[:project_id].eq(project.id))).
join_sources
joins(label_priorities).where(priorities[:priority].eq(nil))
end
def self.left_join_priorities
labels = Label.arel_table
priorities = LabelPriority.arel_table
label_priorities = labels.join(priorities, Arel::Nodes::OuterJoin).
on(labels[:id].eq(priorities[:label_id])).
join_sources
joins(label_priorities)
end end
alias_attribute :name, :title alias_attribute :name, :title
...@@ -77,6 +92,44 @@ class Label < ActiveRecord::Base ...@@ -77,6 +92,44 @@ class Label < ActiveRecord::Base
nil nil
end end
def open_issues_count(user = nil, project = nil)
issues_count(user, project_id: project.try(:id) || project_id, state: 'opened')
end
def closed_issues_count(user = nil, project = nil)
issues_count(user, project_id: project.try(:id) || project_id, state: 'closed')
end
def open_merge_requests_count(user = nil, project = nil)
merge_requests_count(user, project_id: project.try(:id) || project_id, state: 'opened')
end
def prioritize!(project, value)
label_priority = priorities.find_or_initialize_by(project_id: project.id)
label_priority.priority = value
label_priority.save!
end
def unprioritize!(project)
priorities.where(project: project).delete_all
end
def priority(project)
priorities.find_by(project: project).try(:priority)
end
def template?
template
end
def text_color
LabelsHelper.text_color_for_bg(self.color)
end
def title=(value)
write_attribute(:title, sanitize_title(value)) if value.present?
end
## ##
# Returns the String necessary to reference this Label in Markdown # Returns the String necessary to reference this Label in Markdown
# #
...@@ -86,47 +139,45 @@ class Label < ActiveRecord::Base ...@@ -86,47 +139,45 @@ class Label < ActiveRecord::Base
# #
# Label.first.to_reference # => "~1" # Label.first.to_reference # => "~1"
# Label.first.to_reference(format: :name) # => "~\"bug\"" # Label.first.to_reference(format: :name) # => "~\"bug\""
# Label.first.to_reference(project) # => "gitlab-org/gitlab-ce~1" # Label.first.to_reference(project1, project2) # => "gitlab-org/gitlab-ce~1"
# #
# Returns a String # Returns a String
# #
def to_reference(from_project = nil, format: :id) def to_reference(source_project = nil, target_project = nil, format: :id)
format_reference = label_format_reference(format) format_reference = label_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}" reference = "#{self.class.reference_prefix}#{format_reference}"
if cross_project_reference?(from_project) if cross_project_reference?(source_project, target_project)
project.to_reference + reference source_project.to_reference + reference
else else
reference reference
end end
end end
def open_issues_count(user = nil) def as_json(options = {})
issues.visible_to_user(user).opened.count super(options).tap do |json|
json[:priority] = priority(options[:project]) if options.has_key?(:project)
end end
def closed_issues_count(user = nil)
issues.visible_to_user(user).closed.count
end end
def open_merge_requests_count private
merge_requests.opened.count
end
def template? def cross_project_reference?(source_project, target_project)
template source_project && target_project && source_project != target_project
end end
def text_color def issues_count(user, params = {})
LabelsHelper::text_color_for_bg(self.color) IssuesFinder.new(user, params.reverse_merge(label_name: title, scope: 'all'))
.execute
.count
end end
def title=(value) def merge_requests_count(user, params = {})
write_attribute(:title, sanitize_title(value)) if value.present? MergeRequestsFinder.new(user, params.reverse_merge(label_name: title, scope: 'all'))
.execute
.count
end end
private
def label_format_reference(format = :id) def label_format_reference(format = :id)
raise StandardError, 'Unknown format' unless [:id, :name].include?(format) raise StandardError, 'Unknown format' unless [:id, :name].include?(format)
...@@ -137,10 +188,6 @@ class Label < ActiveRecord::Base ...@@ -137,10 +188,6 @@ class Label < ActiveRecord::Base
end end
end end
def nullify_priority
self.priority = nil if priority.blank?
end
def sanitize_title(value) def sanitize_title(value)
CGI.unescapeHTML(Sanitize.clean(value.to_s)) CGI.unescapeHTML(Sanitize.clean(value.to_s))
end end
......
class LabelPriority < ActiveRecord::Base
belongs_to :project
belongs_to :label
validates :project, :label, :priority, presence: true
validates :label_id, uniqueness: { scope: :project_id }
validates :priority, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
end
...@@ -26,6 +26,17 @@ class List < ActiveRecord::Base ...@@ -26,6 +26,17 @@ class List < ActiveRecord::Base
label? ? label.name : list_type.humanize label? ? label.name : list_type.humanize
end end
def as_json(options = {})
super(options).tap do |json|
if options.has_key?(:label)
json[:label] = label.as_json(
project: board.project,
only: [:id, :title, :description, :color]
)
end
end
end
private private
def can_be_destroyed def can_be_destroyed
......
...@@ -121,7 +121,11 @@ class ProjectMember < Member ...@@ -121,7 +121,11 @@ class ProjectMember < Member
end end
def post_destroy_hook def post_destroy_hook
if expired?
event_service.expired_leave_project(self.project, self.user)
else
event_service.leave_project(self.project, self.user) event_service.leave_project(self.project, self.user)
end
super super
end end
......
...@@ -137,6 +137,10 @@ class MergeRequest < ActiveRecord::Base ...@@ -137,6 +137,10 @@ class MergeRequest < ActiveRecord::Base
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end end
def self.project_foreign_key
'target_project_id'
end
# Returns all the merge requests from an ActiveRecord:Relation. # Returns all the merge requests from an ActiveRecord:Relation.
# #
# This method uses a UNION as it usually operates on the result of # This method uses a UNION as it usually operates on the result of
...@@ -322,21 +326,17 @@ class MergeRequest < ActiveRecord::Base ...@@ -322,21 +326,17 @@ class MergeRequest < ActiveRecord::Base
def validate_fork def validate_fork
return true unless target_project && source_project return true unless target_project && source_project
return true if target_project == source_project return true if target_project == source_project
return true unless forked_source_project_missing? return true unless source_project_missing?
errors.add :validate_fork, errors.add :validate_fork,
'Source project is not a fork of the target project' 'Source project is not a fork of the target project'
end end
def closed_without_fork? def closed_without_fork?
closed? && forked_source_project_missing? closed? && source_project_missing?
end
def closed_without_source_project?
closed? && !source_project
end end
def forked_source_project_missing? def source_project_missing?
return false unless for_fork? return false unless for_fork?
return true unless source_project return true unless source_project
...@@ -344,9 +344,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -344,9 +344,7 @@ class MergeRequest < ActiveRecord::Base
end end
def reopenable? def reopenable?
return false if closed_without_fork? || closed_without_source_project? || merged? closed? && !source_project_missing? && source_branch_exists?
closed?
end end
def ensure_merge_request_diff def ensure_merge_request_diff
...@@ -658,7 +656,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -658,7 +656,7 @@ class MergeRequest < ActiveRecord::Base
end end
def has_ci? def has_ci?
source_project.ci_service && commits.any? source_project.try(:ci_service) && commits.any?
end end
def branch_missing? def branch_missing?
...@@ -690,12 +688,9 @@ class MergeRequest < ActiveRecord::Base ...@@ -690,12 +688,9 @@ class MergeRequest < ActiveRecord::Base
@environments ||= @environments ||=
begin begin
environments = source_project.environments_for( envs = target_project.environments_for(target_branch, diff_head_commit, with_tags: true)
source_branch, diff_head_commit) envs.concat(source_project.environments_for(source_branch, diff_head_commit)) if source_project
environments += target_project.environments_for( envs.uniq
target_branch, diff_head_commit, with_tags: true)
environments.uniq
end end
end end
...@@ -787,21 +782,21 @@ class MergeRequest < ActiveRecord::Base ...@@ -787,21 +782,21 @@ class MergeRequest < ActiveRecord::Base
def all_pipelines def all_pipelines
return unless source_project return unless source_project
@all_pipelines ||= begin @all_pipelines ||= source_project.pipelines
sha = if persisted? .where(sha: all_commits_sha, ref: source_branch)
all_commits_sha .order(id: :desc)
else
diff_head_sha
end
source_project.pipelines.order(id: :desc).
where(sha: sha, ref: source_branch)
end
end end
# Note that this could also return SHA from now dangling commits # Note that this could also return SHA from now dangling commits
#
def all_commits_sha def all_commits_sha
if persisted?
merge_request_diffs.flat_map(&:commits_sha).uniq merge_request_diffs.flat_map(&:commits_sha).uniq
elsif compare_commits
compare_commits.to_a.reverse.map(&:id)
else
[diff_head_sha]
end
end end
def merge_commit def merge_commit
......
...@@ -32,8 +32,8 @@ class Project < ActiveRecord::Base ...@@ -32,8 +32,8 @@ class Project < ActiveRecord::Base
default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
after_create :ensure_dir_exist after_create :ensure_dir_exist
after_create :create_project_feature, unless: :project_feature
after_save :ensure_dir_exist, if: :namespace_id_changed? after_save :ensure_dir_exist, if: :namespace_id_changed?
after_initialize :setup_project_feature
# set last_activity_at to the same as created_at # set last_activity_at to the same as created_at
after_create :set_last_activity_at after_create :set_last_activity_at
...@@ -76,6 +76,7 @@ class Project < ActiveRecord::Base ...@@ -76,6 +76,7 @@ class Project < ActiveRecord::Base
has_one :drone_ci_service, dependent: :destroy has_one :drone_ci_service, dependent: :destroy
has_one :emails_on_push_service, dependent: :destroy has_one :emails_on_push_service, dependent: :destroy
has_one :builds_email_service, dependent: :destroy has_one :builds_email_service, dependent: :destroy
has_one :pipelines_email_service, dependent: :destroy
has_one :irker_service, dependent: :destroy has_one :irker_service, dependent: :destroy
has_one :pivotaltracker_service, dependent: :destroy has_one :pivotaltracker_service, dependent: :destroy
has_one :hipchat_service, dependent: :destroy has_one :hipchat_service, dependent: :destroy
...@@ -106,7 +107,7 @@ class Project < ActiveRecord::Base ...@@ -106,7 +107,7 @@ class Project < ActiveRecord::Base
# Merge requests from source project should be kept when source project was removed # Merge requests from source project should be kept when source project was removed
has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: MergeRequest has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: MergeRequest
has_many :issues, dependent: :destroy has_many :issues, dependent: :destroy
has_many :labels, dependent: :destroy has_many :labels, dependent: :destroy, class_name: 'ProjectLabel'
has_many :services, dependent: :destroy has_many :services, dependent: :destroy
has_many :events, dependent: :destroy has_many :events, dependent: :destroy
has_many :milestones, dependent: :destroy has_many :milestones, dependent: :destroy
...@@ -387,6 +388,10 @@ class Project < ActiveRecord::Base ...@@ -387,6 +388,10 @@ class Project < ActiveRecord::Base
Project.count Project.count
end end
end end
def group_ids
joins(:namespace).where(namespaces: { type: 'Group' }).pluck(:namespace_id)
end
end end
def lfs_enabled? def lfs_enabled?
...@@ -663,6 +668,10 @@ class Project < ActiveRecord::Base ...@@ -663,6 +668,10 @@ class Project < ActiveRecord::Base
end end
end end
def issue_reference_pattern
issues_tracker.reference_pattern
end
def default_issues_tracker? def default_issues_tracker?
!external_issue_tracker !external_issue_tracker
end end
...@@ -718,7 +727,7 @@ class Project < ActiveRecord::Base ...@@ -718,7 +727,7 @@ class Project < ActiveRecord::Base
if template.nil? if template.nil?
# If no template, we should create an instance. Ex `create_gitlab_ci_service` # If no template, we should create an instance. Ex `create_gitlab_ci_service`
self.send :"create_#{service_name}_service" public_send("create_#{service_name}_service")
else else
Service.create_from_template(self.id, template) Service.create_from_template(self.id, template)
end end
...@@ -728,10 +737,8 @@ class Project < ActiveRecord::Base ...@@ -728,10 +737,8 @@ class Project < ActiveRecord::Base
def create_labels def create_labels
Label.templates.each do |label| Label.templates.each do |label|
label = label.dup params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
label.template = nil Labels::FindOrCreateService.new(owner, self, params).execute
label.project_id = self.id
label.save
end end
end end
...@@ -1292,7 +1299,7 @@ class Project < ActiveRecord::Base ...@@ -1292,7 +1299,7 @@ class Project < ActiveRecord::Base
environment_ids.where(ref: ref) environment_ids.where(ref: ref)
end end
environments.where(id: environment_ids).select do |environment| environments.available.where(id: environment_ids).select do |environment|
environment.includes_commit?(commit) environment.includes_commit?(commit)
end end
end end
...@@ -1303,11 +1310,6 @@ class Project < ActiveRecord::Base ...@@ -1303,11 +1310,6 @@ class Project < ActiveRecord::Base
"projects/#{id}/pushes_since_gc" "projects/#{id}/pushes_since_gc"
end end
# Prevents the creation of project_feature record for every project
def setup_project_feature
build_project_feature unless project_feature
end
def default_branch_protected? def default_branch_protected?
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL || current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
...@@ -1337,6 +1339,13 @@ class Project < ActiveRecord::Base ...@@ -1337,6 +1339,13 @@ class Project < ActiveRecord::Base
shared_projects.any? shared_projects.any?
end end
# Similar to the normal callbacks that hook into the life cycle of an
# Active Record object, you can also define callbacks that get triggered
# when you add an object to an association collection. If any of these
# callbacks throw an exception, the object will not be added to the
# collection. Before you add a new board to the boards collection if you
# already have 1, 2, or n it will fail, but it if you have 0 that is lower
# than the number of permitted boards per project it won't fail.
def validate_board_limit(board) def validate_board_limit(board)
raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS raise BoardLimitExceeded, 'Number of permitted boards exceeded' if boards.size >= NUMBER_OF_PERMITTED_BOARDS
end end
......
...@@ -13,23 +13,26 @@ class ProjectFeature < ActiveRecord::Base ...@@ -13,23 +13,26 @@ class ProjectFeature < ActiveRecord::Base
# Enabled: enabled for everyone able to access the project # Enabled: enabled for everyone able to access the project
# #
# Permision levels # Permission levels
DISABLED = 0 DISABLED = 0
PRIVATE = 10 PRIVATE = 10
ENABLED = 20 ENABLED = 20
FEATURES = %i(issues merge_requests wiki snippets builds) FEATURES = %i(issues merge_requests wiki snippets builds repository)
# Default scopes force us to unscope here since a service may need to check # Default scopes force us to unscope here since a service may need to check
# permissions for a project in pending_delete # permissions for a project in pending_delete
# http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to # http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
belongs_to :project, -> { unscope(where: :pending_delete) } belongs_to :project, -> { unscope(where: :pending_delete) }
validate :repository_children_level
default_value_for :builds_access_level, value: ENABLED, allows_nil: false 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 :issues_access_level, value: ENABLED, allows_nil: false
default_value_for :merge_requests_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 :snippets_access_level, value: ENABLED, allows_nil: false
default_value_for :wiki_access_level, value: ENABLED, allows_nil: false default_value_for :wiki_access_level, value: ENABLED, allows_nil: false
default_value_for :repository_access_level, value: ENABLED, allows_nil: false
def feature_available?(feature, user) def feature_available?(feature, user)
raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature) raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature)
...@@ -57,6 +60,18 @@ class ProjectFeature < ActiveRecord::Base ...@@ -57,6 +60,18 @@ class ProjectFeature < ActiveRecord::Base
private private
# Validates builds and merge requests access level
# which cannot be higher than repository access level
def repository_children_level
validator = lambda do |field|
level = public_send(field) || ProjectFeature::ENABLED
not_allowed = level > repository_access_level
self.errors.add(field, "cannot have higher visibility level than repository access level") if not_allowed
end
%i(merge_requests_access_level builds_access_level).each(&validator)
end
def get_permission(user, level) def get_permission(user, level)
case level case level
when DISABLED when DISABLED
......
class ProjectLabel < Label
MAX_NUMBER_OF_PRIORITIES = 1
belongs_to :project
validates :project, presence: true
validate :permitted_numbers_of_priorities
validate :title_must_not_exist_at_group_level
delegate :group, to: :project, allow_nil: true
alias_attribute :subject, :project
def to_reference(target_project = nil, format: :id)
super(project, target_project, format: format)
end
private
def title_must_not_exist_at_group_level
return unless group.present? && title_changed?
if group.labels.with_title(self.title).exists?
errors.add(:title, :label_already_exists_at_group_level, group: group.name)
end
end
def permitted_numbers_of_priorities
if priorities && priorities.size > MAX_NUMBER_OF_PRIORITIES
errors.add(:priorities, 'Number of permitted priorities exceeded')
end
end
end
...@@ -43,7 +43,7 @@ class BuildsEmailService < Service ...@@ -43,7 +43,7 @@ class BuildsEmailService < Service
end end
def can_test? def can_test?
project.builds.count > 0 project.builds.any?
end end
def disabled_title def disabled_title
......
class HipchatService < Service class HipchatService < Service
include ActionView::Helpers::SanitizeHelper
MAX_COMMITS = 3 MAX_COMMITS = 3
HIPCHAT_ALLOWED_TAGS = %w[
a b i strong em br img pre code
table th tr td caption colgroup col thead tbody tfoot
ul ol li dl dt dd
]
prop_accessor :token, :room, :server, :notify, :color, :api_version prop_accessor :token, :room, :server, :notify, :color, :api_version
boolean_accessor :notify_only_broken_builds boolean_accessor :notify_only_broken_builds
...@@ -88,6 +95,10 @@ class HipchatService < Service ...@@ -88,6 +95,10 @@ class HipchatService < Service
end end
end end
def render_line(text)
markdown(text.lines.first.chomp, pipeline: :single_line) if text
end
def create_push_message(push) def create_push_message(push)
ref_type = Gitlab::Git.tag_ref?(push[:ref]) ? 'tag' : 'branch' ref_type = Gitlab::Git.tag_ref?(push[:ref]) ? 'tag' : 'branch'
ref = Gitlab::Git.ref_name(push[:ref]) ref = Gitlab::Git.ref_name(push[:ref])
...@@ -110,7 +121,7 @@ class HipchatService < Service ...@@ -110,7 +121,7 @@ class HipchatService < Service
message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)" message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)"
push[:commits].take(MAX_COMMITS).each do |commit| push[:commits].take(MAX_COMMITS).each do |commit|
message << "<br /> - #{commit[:message].lines.first} (<a href=\"#{commit[:url]}\">#{commit[:id][0..5]}</a>)" message << "<br /> - #{render_line(commit[:message])} (<a href=\"#{commit[:url]}\">#{commit[:id][0..5]}</a>)"
end end
if push[:commits].count > MAX_COMMITS if push[:commits].count > MAX_COMMITS
...@@ -121,12 +132,22 @@ class HipchatService < Service ...@@ -121,12 +132,22 @@ class HipchatService < Service
message message
end end
def format_body(body) def markdown(text, options = {})
if body return "" unless text
body = body.truncate(200, separator: ' ', omission: '...')
end context = {
project: project,
pipeline: :email
}
"<pre>#{body}</pre>" Banzai.render(text, context)
context.merge!(options)
html = Banzai.post_process(Banzai.render(text, context), context)
sanitized_html = sanitize(html, tags: HIPCHAT_ALLOWED_TAGS, attributes: %w[href title alt])
sanitized_html.truncate(200, separator: ' ', omission: '...')
end end
def create_issue_message(data) def create_issue_message(data)
...@@ -134,7 +155,7 @@ class HipchatService < Service ...@@ -134,7 +155,7 @@ class HipchatService < Service
obj_attr = data[:object_attributes] obj_attr = data[:object_attributes]
obj_attr = HashWithIndifferentAccess.new(obj_attr) obj_attr = HashWithIndifferentAccess.new(obj_attr)
title = obj_attr[:title] title = render_line(obj_attr[:title])
state = obj_attr[:state] state = obj_attr[:state]
issue_iid = obj_attr[:iid] issue_iid = obj_attr[:iid]
issue_url = obj_attr[:url] issue_url = obj_attr[:url]
...@@ -143,10 +164,7 @@ class HipchatService < Service ...@@ -143,10 +164,7 @@ class HipchatService < Service
issue_link = "<a href=\"#{issue_url}\">issue ##{issue_iid}</a>" issue_link = "<a href=\"#{issue_url}\">issue ##{issue_iid}</a>"
message = "#{user_name} #{state} #{issue_link} in #{project_link}: <b>#{title}</b>" message = "#{user_name} #{state} #{issue_link} in #{project_link}: <b>#{title}</b>"
if description message << "<pre>#{markdown(description)}</pre>"
description = format_body(description)
message << description
end
message message
end end
...@@ -159,23 +177,20 @@ class HipchatService < Service ...@@ -159,23 +177,20 @@ class HipchatService < Service
merge_request_id = obj_attr[:iid] merge_request_id = obj_attr[:iid]
state = obj_attr[:state] state = obj_attr[:state]
description = obj_attr[:description] description = obj_attr[:description]
title = obj_attr[:title] title = render_line(obj_attr[:title])
merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}" merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}"
merge_request_link = "<a href=\"#{merge_request_url}\">merge request !#{merge_request_id}</a>" merge_request_link = "<a href=\"#{merge_request_url}\">merge request !#{merge_request_id}</a>"
message = "#{user_name} #{state} #{merge_request_link} in " \ message = "#{user_name} #{state} #{merge_request_link} in " \
"#{project_link}: <b>#{title}</b>" "#{project_link}: <b>#{title}</b>"
if description message << "<pre>#{markdown(description)}</pre>"
description = format_body(description)
message << description
end
message message
end end
def format_title(title) def format_title(title)
"<b>" + title.lines.first.chomp + "</b>" "<b>#{render_line(title)}</b>"
end end
def create_note_message(data) def create_note_message(data)
...@@ -186,11 +201,13 @@ class HipchatService < Service ...@@ -186,11 +201,13 @@ class HipchatService < Service
note = obj_attr[:note] note = obj_attr[:note]
note_url = obj_attr[:url] note_url = obj_attr[:url]
noteable_type = obj_attr[:noteable_type] noteable_type = obj_attr[:noteable_type]
commit_id = nil
case noteable_type case noteable_type
when "Commit" when "Commit"
commit_attr = HashWithIndifferentAccess.new(data[:commit]) commit_attr = HashWithIndifferentAccess.new(data[:commit])
subject_desc = commit_attr[:id] commit_id = commit_attr[:id]
subject_desc = commit_id
subject_desc = Commit.truncate_sha(subject_desc) subject_desc = Commit.truncate_sha(subject_desc)
subject_type = "commit" subject_type = "commit"
title = format_title(commit_attr[:message]) title = format_title(commit_attr[:message])
...@@ -218,10 +235,7 @@ class HipchatService < Service ...@@ -218,10 +235,7 @@ class HipchatService < Service
message = "#{user_name} commented on #{subject_html} in #{project_link}: " message = "#{user_name} commented on #{subject_html} in #{project_link}: "
message << title message << title
if note message << "<pre>#{markdown(note, ref: commit_id)}</pre>"
note = format_body(note)
message << note
end
message message
end end
......
...@@ -3,6 +3,12 @@ class IssueTrackerService < Service ...@@ -3,6 +3,12 @@ class IssueTrackerService < Service
default_value_for :category, 'issue_tracker' default_value_for :category, 'issue_tracker'
# Pattern used to extract links from comments
# Override this method on services that uses different patterns
def reference_pattern
@reference_pattern ||= %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)}
end
def default? def default?
default default
end end
......
...@@ -13,6 +13,11 @@ class JiraService < IssueTrackerService ...@@ -13,6 +13,11 @@ class JiraService < IssueTrackerService
before_update :reset_password before_update :reset_password
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
def reference_pattern
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
end
def reset_password def reset_password
# don't reset the password if a new one is provided # don't reset the password if a new one is provided
if api_url_changed? && !password_touched? if api_url_changed? && !password_touched?
......
class PipelinesEmailService < Service
prop_accessor :recipients
boolean_accessor :add_pusher
boolean_accessor :notify_only_broken_pipelines
validates :recipients,
presence: true,
if: ->(s) { s.activated? && !s.add_pusher? }
def initialize_properties
self.properties ||= { notify_only_broken_pipelines: true }
end
def title
'Pipelines emails'
end
def description
'Email the pipelines status to a list of recipients.'
end
def to_param
'pipelines_email'
end
def supported_events
%w[pipeline]
end
def execute(data, force: false)
return unless supported_events.include?(data[:object_kind])
return unless force || should_pipeline_be_notified?(data)
all_recipients = retrieve_recipients(data)
return unless all_recipients.any?
pipeline = Ci::Pipeline.find(data[:object_attributes][:id])
Ci::SendPipelineNotificationService.new(pipeline).execute(all_recipients)
end
def can_test?
project.pipelines.any?
end
def disabled_title
'Please setup a pipeline on your repository.'
end
def test_data(project, user)
data = Gitlab::DataBuilder::Pipeline.build(project.pipelines.last)
data[:user] = user.hook_attrs
data
end
def fields
[
{ type: 'textarea',
name: 'recipients',
placeholder: 'Emails separated by comma' },
{ type: 'checkbox',
name: 'add_pusher',
label: 'Add pusher to recipients list' },
{ type: 'checkbox',
name: 'notify_only_broken_pipelines' },
]
end
def test(data)
result = execute(data, force: true)
{ success: true, result: result }
rescue StandardError => error
{ success: false, result: error }
end
def should_pipeline_be_notified?(data)
case data[:object_attributes][:status]
when 'success'
!notify_only_broken_pipelines?
when 'failed'
true
else
false
end
end
def retrieve_recipients(data)
all_recipients = recipients.to_s.split(',').reject(&:blank?)
if add_pusher? && data[:user].try(:[], :email)
all_recipients << data[:user][:email]
end
all_recipients
end
end
...@@ -109,6 +109,10 @@ class Repository ...@@ -109,6 +109,10 @@ class Repository
end end
def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0) def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0)
unless exists? && has_visible_content? && query.present?
return []
end
ref ||= root_ref ref ||= root_ref
args = %W( args = %W(
...@@ -117,9 +121,8 @@ class Repository ...@@ -117,9 +121,8 @@ class Repository
) )
args = args.concat(%W(-- #{path})) if path.present? args = args.concat(%W(-- #{path})) if path.present?
git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:chomp) git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines
commits = git_log_results.map { |c| commit(c) } git_log_results.map { |c| commit(c.chomp) }.compact
commits
end end
def find_branch(name, fresh_repo: true) def find_branch(name, fresh_repo: true)
...@@ -416,6 +419,17 @@ class Repository ...@@ -416,6 +419,17 @@ class Repository
@exists = nil @exists = nil
end end
# expire cache that doesn't depend on repository data (when expiring)
def expire_content_cache
expire_tags_cache
expire_tag_count_cache
expire_branches_cache
expire_branch_count_cache
expire_root_ref_cache
expire_emptiness_caches
expire_exists_cache
end
# Runs code after a repository has been created. # Runs code after a repository has been created.
def after_create def after_create
expire_exists_cache expire_exists_cache
...@@ -431,14 +445,7 @@ class Repository ...@@ -431,14 +445,7 @@ class Repository
expire_cache if exists? expire_cache if exists?
# expire cache that don't depend on repository data (when expiring) expire_content_cache
expire_tags_cache
expire_tag_count_cache
expire_branches_cache
expire_branch_count_cache
expire_root_ref_cache
expire_emptiness_caches
expire_exists_cache
repository_event(:remove_repository) repository_event(:remove_repository)
end end
...@@ -470,14 +477,13 @@ class Repository ...@@ -470,14 +477,13 @@ class Repository
end end
def before_import def before_import
expire_emptiness_caches expire_content_cache
expire_exists_cache
end end
# Runs code after a repository has been forked/imported. # Runs code after a repository has been forked/imported.
def after_import def after_import
expire_emptiness_caches expire_content_cache
expire_exists_cache build_cache
end end
# Runs code after a new commit has been pushed. # Runs code after a new commit has been pushed.
......
...@@ -196,12 +196,13 @@ class Service < ActiveRecord::Base ...@@ -196,12 +196,13 @@ class Service < ActiveRecord::Base
end end
def self.available_services_names def self.available_services_names
%w( %w[
asana asana
assembla assembla
bamboo bamboo
buildkite buildkite
builds_email builds_email
pipelines_email
bugzilla bugzilla
campfire campfire
custom_issue_tracker custom_issue_tracker
...@@ -218,7 +219,7 @@ class Service < ActiveRecord::Base ...@@ -218,7 +219,7 @@ class Service < ActiveRecord::Base
redmine redmine
slack slack
teamcity teamcity
) ]
end end
def self.create_from_template(project_id, template) def self.create_from_template(project_id, template)
......
...@@ -52,7 +52,13 @@ class Todo < ActiveRecord::Base ...@@ -52,7 +52,13 @@ class Todo < ActiveRecord::Base
# Todos with highest priority first then oldest todos # Todos with highest priority first then oldest todos
# Need to order by created_at last because of differences on Mysql and Postgres when joining by type "Merge_request/Issue" # Need to order by created_at last because of differences on Mysql and Postgres when joining by type "Merge_request/Issue"
def order_by_labels_priority def order_by_labels_priority
highest_priority = highest_label_priority(["Issue", "MergeRequest"], "todos.target_id").to_sql params = {
target_type: ['Issue', 'MergeRequest'],
target_column: "todos.target_id",
project_column: "todos.project_id"
}
highest_priority = highest_label_priority(params).to_sql
select("#{table_name}.*, (#{highest_priority}) AS highest_priority"). select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')). order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')).
......
class GroupLabelPolicy < BasePolicy
def rules
delegate! @subject.group
end
end
...@@ -19,6 +19,7 @@ class GroupPolicy < BasePolicy ...@@ -19,6 +19,7 @@ class GroupPolicy < BasePolicy
if master if master
can! :create_projects can! :create_projects
can! :admin_milestones can! :admin_milestones
can! :admin_label
end end
# Only group owner and administrators can admin group # Only group owner and administrators can admin group
......
class ProjectLabelPolicy < BasePolicy
def rules
delegate! @subject.project
end
end
...@@ -162,11 +162,13 @@ class ProjectPolicy < BasePolicy ...@@ -162,11 +162,13 @@ class ProjectPolicy < BasePolicy
end end
def disabled_features! def disabled_features!
repository_enabled = project.feature_available?(:repository, user)
unless project.feature_available?(:issues, user) unless project.feature_available?(:issues, user)
cannot!(*named_abilities(:issue)) cannot!(*named_abilities(:issue))
end end
unless project.feature_available?(:merge_requests, user) unless project.feature_available?(:merge_requests, user) && repository_enabled
cannot!(*named_abilities(:merge_request)) cannot!(*named_abilities(:merge_request))
end end
...@@ -183,13 +185,21 @@ class ProjectPolicy < BasePolicy ...@@ -183,13 +185,21 @@ class ProjectPolicy < BasePolicy
cannot!(*named_abilities(:wiki)) cannot!(*named_abilities(:wiki))
end end
unless project.feature_available?(:builds, user) unless project.feature_available?(:builds, user) && repository_enabled
cannot!(*named_abilities(:build)) cannot!(*named_abilities(:build))
cannot!(*named_abilities(:pipeline)) cannot!(*named_abilities(:pipeline))
cannot!(*named_abilities(:environment)) cannot!(*named_abilities(:environment))
cannot!(*named_abilities(:deployment)) cannot!(*named_abilities(:deployment))
end end
unless repository_enabled
cannot! :push_code
cannot! :push_code_to_protected_branches
cannot! :download_code
cannot! :fork_project
cannot! :read_commit_status
end
unless project.container_registry_enabled unless project.container_registry_enabled
cannot!(*named_abilities(:container_image)) cannot!(*named_abilities(:container_image))
end end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment