Commit e5de4c77 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into pipeline-notifications

* upstream/master: (221 commits)
  Differentiate the expire from leave event
  Use LabelsFinder on Fogbuz importer
  Use LabelsFinder on Google Code importer
  Change the order of tested methods in project_members_controller_spec
  Remove show_menu_above attribute from issuable dropdowns.
  Spaces before `}`!
  Avoid touch label links that does not belongs to project when moving it
  Remove order by label type on LabelsFinder
  Rename Labels::CreateService to Labels::FindOrCreateService
  Add self.project_foreign_key on both Issue and MergeRequest
  Only show label type for projects that belong to a group
  Disable subscribing to group-level labels
  Remove unused method Project#all_labels
  Update specs to cope with new label types and priorities
  Fix max number of permitted priorities per project label
  Fix GitHub importer spec
  Update CHANGELOG
  Add support to group labels to SlashCommands::InterpretService
  Use join instead of subquery on Label.unprioritized scope
  Warn user deleting a group label affect all projects within the group
  ...
parents cefc7eb1 b5cee666
Please view this file on the master branch, on stable branches it's out of date.
## 8.14.0 (2016-11-22)
- Adds user project membership expired event to clarify why user was removed (Callum Dryden)
## 8.13.0 (2016-10-22)
- Fix save button on project pipeline settings page. (!6955)
- Avoid race condition when asynchronously removing expired artifacts. (!6881)
- Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675)
- Respond with 404 Not Found for non-existent tags (Linus Thiel)
- Truncate long labels with ellipsis in labels page
......@@ -11,11 +16,15 @@ Please view this file on the master branch, on stable branches it's out of date.
- Update runner version only when updating contacted_at
- Add link from system note to compare with previous version
- 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
- Add `/projects/visible` API endpoint (Ben Boeckel)
- Fix centering of custom header logos (Ashley Dumaine)
- Keep around commits only pipeline creation as pipeline data doesn't change over time
- ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
- Add group level labels. (!6425)
- 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
- Clarify documentation for Runners API (Gennady Trafimenkov)
- The instrumentation for Banzai::Renderer has been restored
......@@ -54,6 +63,7 @@ Please view this file on the master branch, on stable branches it's out of date.
- Add Issue Board API support (andrebsguedes)
- Allow the Koding integration to be configured through the API
- Add new issue button to each list on Issues Board
- Execute specific named route method from toggle_award_url helper method
- Added soft wrap button to repository file/blob editor
- Update namespace validation to forbid reserved names (.git and .atom) (Will Starms)
- Show the time ago a merge request was deployed to an environment
......@@ -63,6 +73,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)
- Remove redundant mixins (ClemMakesApps)
- 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)
- Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska)
- Fix that manual jobs would no longer block jobs in the next stage. !6604
......@@ -101,10 +112,10 @@ Please view this file on the master branch, on stable branches it's out of date.
- Optimize GitHub importing for speed and memory
- API: expose pipeline data in builds API (!6502, Guilherme Salazar)
- 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
- 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 the diff in the merge request view when converting a symlink to a regular file
- Fix Pipeline list commit column width should be adjusted
- 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)
......@@ -112,13 +123,17 @@ Please view this file on the master branch, on stable branches it's out of date.
- Retouch environments list and deployments list
- 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 Nofollow for uppercased scheme in external urls !6820 (the-undefined)
- Allow empty merge requests !6384 (Artem Sidorenko)
- Grouped pipeline dropdown is a scrollable container
- Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi)
- Fixes padding in all clipboard icons that have .btn class
- 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
- Add docs for request profiling
- Delete dynamic environments
- Fix buggy iOS tooltip layering behavior.
- Make guests unable to view MRs on private projects
## 8.12.7
......
......@@ -29,7 +29,7 @@ gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.0'
gem 'omniauth-google-oauth2', '~> 0.4.1'
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-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0'
......
......@@ -473,9 +473,9 @@ GEM
omniauth-oauth2 (1.3.1)
oauth2 (~> 1.0)
omniauth (~> 1.2)
omniauth-saml (1.6.0)
omniauth-saml (1.7.0)
omniauth (~> 1.3)
ruby-saml (~> 1.3)
ruby-saml (~> 1.4)
omniauth-shibboleth (1.2.1)
omniauth (>= 1.0.0)
omniauth-twitter (1.2.1)
......@@ -635,7 +635,7 @@ GEM
crack (~> 0.4)
ruby-prof (0.16.2)
ruby-progressbar (1.8.1)
ruby-saml (1.3.0)
ruby-saml (1.4.1)
nokogiri (>= 1.5.10)
ruby_parser (3.8.2)
sexp_processor (~> 4.1)
......@@ -915,7 +915,7 @@ DEPENDENCIES
omniauth-gitlab (~> 1.0.0)
omniauth-google-oauth2 (~> 0.4.1)
omniauth-kerberos (~> 0.3.0)
omniauth-saml (~> 1.6.0)
omniauth-saml (~> 1.7.0)
omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0)
omniauth_crowd (~> 2.2.0)
......
8.13.0-pre
8.14.0-pre
//= require vue
((global) => {
const COOKIE_NAME = 'cycle_analytics_help_dismissed';
......
......@@ -168,6 +168,8 @@
shortcut_handler = new ShortcutsNavigation();
new ShortcutsBlob(true);
break;
case 'groups:labels:new':
case 'groups:labels:edit':
case 'projects:labels:new':
case 'projects:labels:edit':
new Labels();
......
......@@ -266,7 +266,7 @@
},
fieldName: $dropdown.data('field-name'),
id: function(label) {
if (label.id <= 0) return;
if (label.id <= 0) return label.title;
if ($dropdown.hasClass('js-issuable-form-dropdown')) {
return label.id;
......
......@@ -17,6 +17,12 @@
View on <%- external_url_formatted %>
</a>
</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>`;
......@@ -205,6 +211,11 @@
if ($(`.mr-state-widget #${ environment.id }`).length) return;
const $template = $(DEPLOYMENT_TEMPLATE);
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) {
environment.deployed_at = $.timeago(environment.deployed_at) + '.';
} else {
......
......@@ -4,9 +4,8 @@
this.ProjectNew = (function() {
function ProjectNew() {
this.toggleSettings = bind(this.toggleSettings, this);
this.$selects = $('.features select').filter(function () {
return $(this).data('field');
});
this.$selects = $('.features select');
this.$repoSelects = this.$selects.filter('.js-repo-select');
$('.project-edit-container').on('ajax:before', (function(_this) {
return function() {
......@@ -16,6 +15,7 @@
})(this));
this.toggleSettings();
this.toggleSettingsOnclick();
this.toggleRepoVisibility();
}
ProjectNew.prototype.toggleSettings = function() {
......@@ -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;
})();
......
......@@ -167,7 +167,6 @@
*/
&.code {
padding: 0;
-webkit-overflow-scrolling: auto; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987
}
}
}
......
......@@ -27,3 +27,15 @@ body {
.container-limited {
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;
}
......@@ -16,6 +16,7 @@ $white-light: #fff;
$white-normal: #ededed;
$white-dark: #ececec;
$gray-lightest: #fdfdfd;
$gray-light: #fafafa;
$gray-lighter: #f9f9f9;
$gray-normal: #f5f5f5;
......
lex
[v-cloak] {
display: none;
}
......@@ -132,7 +131,7 @@ lex
}
.board-blank-state {
height: 100%;
height: calc(100% - 49px);
padding: $gl-padding;
background-color: #fff;
}
......
......@@ -195,7 +195,7 @@
.build-job {
position: relative;
.fa {
.fa-arrow-right {
position: absolute;
left: 15px;
top: 20px;
......@@ -205,14 +205,23 @@
&.active {
font-weight: bold;
.fa {
.fa-arrow-right {
display: block;
}
}
&.retried {
background-color: $gray-lightest;
}
&:hover {
background-color: $row-hover;
}
.fa-refresh {
font-size: 13px;
margin-left: 3px;
}
}
}
}
......
......@@ -52,7 +52,6 @@
background: #fff;
color: #333;
border-radius: 0 0 3px 3px;
-webkit-overflow-scrolling: auto;
.unfold {
cursor: pointer;
......
......@@ -38,6 +38,14 @@
color: $gl-dark-link-color;
}
.stop-env-link {
color: $table-text-gray;
.stop-env-icon {
font-size: 14px;
}
}
.deployment {
.build-column {
......
......@@ -66,7 +66,21 @@
text-overflow: ellipsis;
vertical-align: middle;
max-width: 100%;
}
}
}
.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 {
......@@ -209,6 +223,13 @@
}
.label-subscribe-button {
.label-subscribe-button-icon {
&[disabled] {
opacity: 0.5;
pointer-events: none;
}
}
.label-subscribe-button-loading {
display: none;
}
......
......@@ -124,15 +124,20 @@
display: -webkit-flex;
display: flex;
box-shadow: 0 0 0 1px $border-color;
border-top-right-radius: 2px;
border-top-left-radius: 2px;
border-top-right-radius: $border-radius-default;
border-top-left-radius: $border-radius-default;
li {
flex: 1;
text-align: center;
&:first-of-type {
border-top-left-radius: $border-radius-default;
}
&:last-of-type {
border-left: 1px solid $border-color;
border-top-right-radius: $border-radius-default;
}
&:not(.active) {
......
......@@ -183,6 +183,15 @@
.ci-coverage {
float: right;
}
.stop-env-container {
color: $gl-text-color;
float: right;
a {
color: $gl-text-color;
}
}
}
.mr_source_commit,
......
......@@ -23,10 +23,17 @@
.table.ci-table {
min-width: 1200px;
.branch-commit {
width: 33%;
.pipeline-id {
color: $black;
}
.branch-commit {
width: 30%;
.branch-name {
max-width: 195px;
}
}
}
}
......@@ -73,6 +80,10 @@
border-top-width: 1px;
}
.build.retried {
background-color: $gray-lightest;
}
.commit-link {
.ci-status {
......@@ -88,6 +99,15 @@
}
}
.avatar {
margin-left: 0;
float: none;
}
.api {
color: $code-color;
}
.branch-commit {
.branch-name {
......@@ -109,12 +129,11 @@
.fa {
font-size: 12px;
color: $table-text-gray;
color: $gl-text-color;
}
.commit-id {
color: $gl-link-color;
margin-right: 8px;
}
.commit-title {
......@@ -125,10 +144,6 @@
text-overflow: ellipsis;
}
.avatar {
margin-left: 0;
}
.label {
margin-right: 4px;
}
......@@ -144,17 +159,11 @@
.icon-container {
display: inline-block;
text-align: right;
width: 15px;
width: 10px;
.fa {
position: relative;
right: 3px;
}
svg {
position: relative;
right: 1px;
&.commit-icon {
width: 15px;
text-align: center;
}
}
......@@ -634,6 +643,10 @@
&.pipelines {
.ci-table {
min-width: 900px;
}
.content-list.pipelines {
overflow: auto;
}
......
......@@ -761,62 +761,6 @@ pre.light-well {
.dropdown-menu {
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 {
......@@ -855,3 +799,30 @@ pre.light-well {
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;
}
}
......@@ -2,6 +2,7 @@ module IssuableActions
extend ActiveSupport::Concern
included do
before_action :labels, only: [:show, :new, :edit]
before_action :authorize_destroy_issuable!, only: :destroy
before_action :authorize_admin_issuable!, only: :bulk_update
end
......@@ -25,6 +26,10 @@ module IssuableActions
private
def labels
@labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
end
def authorize_destroy_issuable!
unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable)
return access_denied!
......
class Dashboard::LabelsController < Dashboard::ApplicationController
def index
labels = Label.where(project_id: projects).select(:id, :title, :color).uniq(:title)
labels = LabelsFinder.new(current_user).execute
respond_to do |format|
format.json { render json: labels }
format.json { render json: labels.as_json(only: [:id, :title, :color]) }
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
def serialize_as_json(resource)
resource.as_json(
labels: true,
only: [:iid, :title, :confidential],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
assignee: { only: [:id, :name, :username], methods: [:avatar_url] }
})
end
end
......
......@@ -76,9 +76,8 @@ module Projects
resource.as_json(
only: [:id, :list_type, :position],
methods: [:title],
include: {
label: { only: [:id, :title, :description, :color, :priority] }
})
label: true
)
end
end
end
......
......@@ -2,11 +2,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController
layout 'project'
before_action :authorize_read_environment!
before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_update_environment!, only: [:edit, :update, :destroy]
before_action :environment, only: [:show, :edit, :update, :destroy]
before_action :authorize_create_deployment!, only: [:stop]
before_action :authorize_update_environment!, only: [:edit, :update]
before_action :environment, only: [:show, :edit, :update, :stop]
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
def show
......@@ -38,14 +46,11 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
end
def destroy
if @environment.destroy
flash[:notice] = 'Environment was successfully removed.'
else
flash[:alert] = 'Failed to remove environment.'
end
def stop
return render_404 unless @environment.stoppable?
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
private
......
......@@ -26,7 +26,9 @@ class Projects::IssuesController < Projects::ApplicationController
@issues = issues_collection
@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|
format.html
......
......@@ -3,21 +3,22 @@ class Projects::LabelsController < Projects::ApplicationController
before_action :module_enabled
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_admin_labels!, only: [
:new, :create, :edit, :update, :generate, :destroy, :remove_priority, :set_priorities
]
before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update,
:generate, :destroy, :remove_priority,
:set_priorities]
respond_to :js, :html
def index
@labels = @project.labels.unprioritized.page(params[:page])
@prioritized_labels = @project.labels.prioritized
@prioritized_labels = @available_labels.prioritized(@project)
@labels = @available_labels.unprioritized(@project).page(params[:page])
respond_to do |format|
format.html
format.json do
render json: @project.labels
render json: @available_labels.as_json(only: [:id, :title, :color])
end
end
end
......@@ -36,7 +37,7 @@ class Projects::LabelsController < Projects::ApplicationController
end
else
respond_to do |format|
format.html { render 'new' }
format.html { render :new }
format.json { render json: { message: @label.errors.messages }, status: 400 }
end
end
......@@ -49,7 +50,7 @@ class Projects::LabelsController < Projects::ApplicationController
if @label.update_attributes(label_params)
redirect_to namespace_project_labels_path(@project.namespace, @project)
else
render 'edit'
render :edit
end
end
......@@ -68,6 +69,7 @@ class Projects::LabelsController < Projects::ApplicationController
def destroy
@label.destroy
@labels = find_labels
respond_to do |format|
format.html do
......@@ -80,20 +82,24 @@ class Projects::LabelsController < Projects::ApplicationController
def remove_priority
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 }
else
message = label.errors.full_messages.uniq.join('. ')
format.json { render json: { message: message }, status: :unprocessable_entity }
format.json { head :unprocessable_entity }
end
end
end
def set_priorities
Label.transaction do
params[:label_ids].each_with_index do |label_id, index|
label = @project.labels.find_by_id(label_id)
label.update_attribute(:priority, index) if label
available_labels_ids = @available_labels.where(id: params[:label_ids]).pluck(:id)
label_ids = params[:label_ids].select { |id| available_labels_ids.include?(id.to_i) }
label_ids.each_with_index do |label_id, index|
label = @available_labels.find(label_id)
label.prioritize!(project, index)
end
end
......@@ -119,6 +125,10 @@ class Projects::LabelsController < Projects::ApplicationController
end
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!
return render_404 unless can?(current_user, :admin_label, @project)
end
......
......@@ -40,7 +40,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_requests = @merge_requests.page(params[:page])
@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|
format.html
......@@ -422,10 +425,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController
project = environment.project
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,
name: environment.name,
url: namespace_project_environment_path(project.namespace, project, environment),
stop_url: stop_url,
external_url: environment.external_url,
external_url_formatted: environment.formatted_external_url,
deployed_at: deployment.try(:created_at),
......@@ -483,13 +492,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@noteable = @merge_request
@commits_count = @merge_request.commits.count
@pipeline = @merge_request.pipeline
@statuses = @pipeline.statuses.relevant if @pipeline
if @merge_request.locked_long_ago?
@merge_request.unlock_mr
@merge_request.close
end
define_pipelines_vars
end
# Discussion tab data is rendered on html responses of actions
......@@ -517,7 +525,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_widget_vars
@pipeline = @merge_request.pipeline
@pipelines = [@pipeline].compact
end
def define_commit_vars
......@@ -544,6 +551,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController
)
end
def define_pipelines_vars
@pipelines = @merge_request.all_pipelines
if @pipelines.any?
@pipeline = @pipelines.first
@statuses = @pipeline.statuses.relevant
end
end
def define_new_vars
@noteable = @merge_request
......@@ -559,10 +575,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit
@pipeline = @merge_request.pipeline
@statuses = @pipeline.statuses.relevant if @pipeline
@note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
@labels = LabelsFinder.new(current_user, project_id: @project.id).execute
define_pipelines_vars
end
def invalid_mr
......
class ProjectsController < Projects::ApplicationController
include IssuableCollections
include ExtractsPath
before_action :authenticate_user!, except: [:show, :activity, :refs]
......@@ -103,16 +104,7 @@ class ProjectsController < Projects::ApplicationController
respond_to do |format|
format.html do
@notification_setting = current_user.notification_settings_for(@project) if current_user
if @project.repository_exists?
if @project.empty_repo?
render 'projects/empty'
else
render :show
end
else
render 'projects/no_repo'
end
render_landing_page
end
format.atom do
......@@ -285,6 +277,26 @@ class ProjectsController < Projects::ApplicationController
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
if [:new, :create].include?(action_name.to_sym)
'application'
......@@ -308,7 +320,8 @@ class ProjectsController < Projects::ApplicationController
project_feature_attributes:
[
: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,15 +124,12 @@ class IssuableFinder
def labels
return @labels if defined?(@labels)
if labels? && !filter_by_no_label?
@labels = Label.where(title: label_names)
if projects
@labels = @labels.where(project: projects)
@labels =
if labels? && !filter_by_no_label?
LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute
else
Label.none
end
else
@labels = Label.none
end
end
def assignee?
......@@ -274,8 +271,10 @@ class IssuableFinder
items = items.without_label
else
items = items.with_label(label_names, params[:sort])
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
......
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
def toggle_award_url(awardable)
if @project
url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
return url_for([:toggle_award_emoji, awardable]) unless @project
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 (6.5x)
toggle_award_emoji_namespace_project_note_url(namespace_id: @project.namespace, project_id: @project, id: awardable.id)
else
url_for([:toggle_award_emoji, awardable])
url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
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
end
def event_commit_title(message)
escape_once(truncate(message.split("\n").first, length: 70))
(message.split("\n").first || "").truncate(70)
rescue
"--broken encoding"
end
......
......@@ -124,6 +124,10 @@ module IssuablesHelper
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)
issuables_finder = public_send("#{issuable_type}_finder")
issuables_finder.params[:state] = state
......
......@@ -4,9 +4,8 @@ module LabelsHelper
# Link to a Label
#
# label - Label object to link to
# project - Project object which will be used as the context for the label's
# link. If omitted, defaults to `@project`, or the label's own
# project.
# subject - Project/Group object which will be used as the context for the
# label's link. If omitted, defaults to the label's own group/project.
# type - The type of item the link will point to (:issue or
# :merge_request). If omitted, defaults to :issue.
# block - An optional block that will be passed to `link_to`, forming the
......@@ -15,15 +14,14 @@ module LabelsHelper
#
# 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)
#
# # Force the generated link to use @project
# @project = Project.first
# link_to_label(label)
# # Force the generated link to use a provided group
# link_to_label(label, subject: Group.last)
#
# # 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
# link_to_label(label, type: :merge_request)
......@@ -32,9 +30,8 @@ module LabelsHelper
# link_to_label(label) { "My Custom Label Text" }
#
# Returns a String
def link_to_label(label, project: nil, type: :issue, tooltip: true, css_class: nil, &block)
project ||= @project || label.project
link = label_filter_path(project, label, type: type)
def link_to_label(label, subject: nil, type: :issue, tooltip: true, css_class: nil, &block)
link = label_filter_path(subject || label.subject, label, type: type)
if block_given?
link_to link, class: css_class, &block
......@@ -43,15 +40,40 @@ module LabelsHelper
end
end
def label_filter_path(project, label, type: issue)
send("namespace_project_#{type.to_s.pluralize}_path",
project.namespace,
project,
label_name: [label.name])
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",
subject.namespace,
subject,
label_name: [label.name])
end
end
def edit_label_path(label)
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 project_label_names
@project.labels.pluck(:title)
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
def render_colored_label(label, label_suffix = '', tooltip: true)
......@@ -68,8 +90,8 @@ module LabelsHelper
span.html_safe
end
def render_colored_cross_project_label(label, tooltip: true)
label_suffix = label.project.name_with_namespace
def render_colored_cross_project_label(label, source_project = nil, tooltip: true)
label_suffix = source_project ? source_project.name_with_namespace : label.project.name_with_namespace
label_suffix = " <i>in #{escape_once(label_suffix)}</i>"
render_colored_label(label, label_suffix, tooltip: tooltip)
end
......@@ -115,7 +137,10 @@ module LabelsHelper
end
def labels_filter_path
return group_labels_path(@group, :json) if @group
project = @target_project || @project
if project
namespace_project_labels_path(project.namespace, project, :json)
else
......@@ -124,11 +149,24 @@ module LabelsHelper
end
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
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
# Required for Banzai::Filter::LabelReferenceFilter
......
......@@ -50,6 +50,20 @@ module PreferencesHelper
end
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
......@@ -134,16 +134,35 @@ module ProjectsHelper
options = project_feature_options
if @project.private?
level = @project.project_feature.send(field)
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
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
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)
nav_tabs = [:home]
......@@ -155,12 +174,8 @@ module ProjectsHelper
nav_tabs << :merge_requests
end
if can?(current_user, :read_pipeline, project)
nav_tabs << :pipelines
end
if can?(current_user, :read_build, project)
nav_tabs << :builds
nav_tabs << :pipelines
end
if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project)
......@@ -435,4 +450,8 @@ module ProjectsHelper
'Everyone with access' => ProjectFeature::ENABLED
}
end
def project_child_container_class(view_path)
view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}"
end
end
......@@ -19,7 +19,7 @@ module Ci
validates_presence_of :status, 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
......@@ -165,7 +165,7 @@ module Ci
def retryable?
builds.latest.any? do |build|
build.failed? && build.retryable?
(build.failed? || build.canceled?) && build.retryable?
end
end
......
......@@ -5,11 +5,15 @@ module Expirable
scope :expired, -> { where('expires_at <= ?', Time.current) }
end
def expired?
expires? && expires_at <= Time.current
end
def expires?
expires_at.present?
end
def expires_soon?
expires_at < 7.days.from_now
expires? && expires_at < 7.days.from_now
end
end
......@@ -5,6 +5,7 @@ module HasStatus
STARTED_STATUSES = %w[running success failed skipped]
ACTIVE_STATUSES = %w[pending running]
COMPLETED_STATUSES = %w[success failed canceled]
ORDERED_STATUSES = %w[failed pending running canceled success skipped]
class_methods do
def status_sql
......
......@@ -145,8 +145,14 @@ module Issuable
end
def order_labels_priority(excluded_labels: [])
condition_field = "#{table_name}.id"
highest_priority = highest_label_priority(name, condition_field, excluded_labels: excluded_labels).to_sql
params = {
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").
group(arel_table[:id]).
......@@ -230,18 +236,6 @@ module Issuable
labels.order('title ASC').pluck(:title)
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
#
# Examples:
......
......@@ -38,11 +38,13 @@ module Sortable
private
def highest_label_priority(object_types, condition_field, excluded_labels: [])
query = Label.select(Label.arel_table[:priority].minimum).
def highest_label_priority(target_type:, target_column:, project_column:, excluded_labels: [])
query = Label.select(LabelPriority.arel_table[:priority].minimum).
left_join_priorities.
joins(:label_links).
where(label_links: { target_type: object_types }).
where("label_links.target_id = #{condition_field}").
where("label_priorities.project_id = #{project_column}").
where(label_links: { target_type: target_type }).
where("label_links.target_id = #{target_column}").
reorder(nil)
query.where.not(title: excluded_labels) if excluded_labels.present?
......
......@@ -34,7 +34,7 @@ class Deployment < ActiveRecord::Base
end
def manual_actions
deployable.try(:other_actions)
@manual_actions ||= deployable.try(:other_actions)
end
def includes_commit?(commit)
......@@ -84,6 +84,17 @@ class Deployment < ActiveRecord::Base
take
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
created_at.to_time.in_time_zone.to_s(:medium)
end
......
......@@ -19,6 +19,24 @@ class Environment < ActiveRecord::Base
allow_nil: 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
deployments.last
end
......@@ -66,4 +84,14 @@ class Environment < ActiveRecord::Base
external_url.gsub(/\A.*?:\/\//, '')
end
def stoppable?
available? && stop_action.present?
end
def stop!(current_user)
return unless stoppable?
stop_action.play(current_user)
end
end
......@@ -12,6 +12,7 @@ class Event < ActiveRecord::Base
JOINED = 8 # User joined project
LEFT = 9 # User left project
DESTROYED = 10
EXPIRED = 11 # User left project due to expiry
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
......@@ -115,6 +116,10 @@ class Event < ActiveRecord::Base
action == LEFT
end
def expired?
action == EXPIRED
end
def destroyed?
action == DESTROYED
end
......@@ -124,7 +129,7 @@ class Event < ActiveRecord::Base
end
def membership_changed?
joined? || left?
joined? || left? || expired?
end
def created_project?
......@@ -184,6 +189,8 @@ class Event < ActiveRecord::Base
'joined'
elsif left?
'left'
elsif expired?
'removed due to membership expiration from'
elsif destroyed?
'destroyed'
elsif commented?
......
......@@ -29,11 +29,6 @@ class ExternalIssue
@project
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)
id
end
......
......@@ -19,6 +19,7 @@ class Group < Namespace
has_many :project_group_links, dependent: :destroy
has_many :shared_projects, through: :project_group_links, source: :project
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 :visibility_level_allowed_by_projects
......
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
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end
def self.project_foreign_key
'project_id'
end
def self.sort(method, excluded_labels: [])
case method.to_s
when 'due_date_asc' then order_due_date_asc
......@@ -274,4 +278,16 @@ class Issue < ActiveRecord::Base
def check_for_spam?
project.public?
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
......@@ -15,34 +15,49 @@ class Label < ActiveRecord::Base
default_value_for :color, DEFAULT_COLOR
belongs_to :project
has_many :lists, dependent: :destroy
has_many :priorities, class_name: 'LabelPriority'
has_many :label_links, dependent: :destroy
has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
validates :color, color: true, allow_blank: false
validates :project, presence: true, unless: Proc.new { |service| service.template? }
# Don't allow ',' for label titles
validates :title,
presence: true,
format: { with: /\A[^,]+\z/ },
uniqueness: { scope: :project_id }
before_save :nullify_priority
validates :title, presence: true, format: { with: /\A[^,]+\z/ }
validates :title, uniqueness: { scope: [:group_id, :project_id] }
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(project)
joins(:priorities)
.where(label_priorities: { project_id: project })
.reorder('label_priorities.priority ASC, labels.title ASC')
end
def self.unprioritized(project)
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
def self.prioritized
where.not(priority: nil).reorder(:priority, :title)
joins(label_priorities).where(priorities[:priority].eq(nil))
end
def self.unprioritized
where(priority: nil)
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
alias_attribute :name, :title
......@@ -77,6 +92,44 @@ class Label < ActiveRecord::Base
nil
end
def open_issues_count(user = nil, project = nil)
issues_count(user, project_id: project.try(:id) || project_id, state: 'opened')
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
#
......@@ -84,49 +137,47 @@ class Label < ActiveRecord::Base
#
# Examples:
#
# Label.first.to_reference # => "~1"
# Label.first.to_reference(format: :name) # => "~\"bug\""
# Label.first.to_reference(project) # => "gitlab-org/gitlab-ce~1"
# Label.first.to_reference # => "~1"
# Label.first.to_reference(format: :name) # => "~\"bug\""
# Label.first.to_reference(project1, project2) # => "gitlab-org/gitlab-ce~1"
#
# 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)
reference = "#{self.class.reference_prefix}#{format_reference}"
if cross_project_reference?(from_project)
project.to_reference + reference
if cross_project_reference?(source_project, target_project)
source_project.to_reference + reference
else
reference
end
end
def open_issues_count(user = nil)
issues.visible_to_user(user).opened.count
end
def closed_issues_count(user = nil)
issues.visible_to_user(user).closed.count
def as_json(options = {})
super(options).tap do |json|
json[:priority] = priority(options[:project]) if options.has_key?(:project)
end
end
def open_merge_requests_count
merge_requests.opened.count
end
private
def template?
template
def cross_project_reference?(source_project, target_project)
source_project && target_project && source_project != target_project
end
def text_color
LabelsHelper::text_color_for_bg(self.color)
def issues_count(user, params = {})
IssuesFinder.new(user, params.reverse_merge(label_name: title, scope: 'all'))
.execute
.count
end
def title=(value)
write_attribute(:title, sanitize_title(value)) if value.present?
def merge_requests_count(user, params = {})
MergeRequestsFinder.new(user, params.reverse_merge(label_name: title, scope: 'all'))
.execute
.count
end
private
def label_format_reference(format = :id)
raise StandardError, 'Unknown format' unless [:id, :name].include?(format)
......@@ -137,10 +188,6 @@ class Label < ActiveRecord::Base
end
end
def nullify_priority
self.priority = nil if priority.blank?
end
def sanitize_title(value)
CGI.unescapeHTML(Sanitize.clean(value.to_s))
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
label? ? label.name : list_type.humanize
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
def can_be_destroyed
......
......@@ -121,7 +121,11 @@ class ProjectMember < Member
end
def post_destroy_hook
event_service.leave_project(self.project, self.user)
if expired?
event_service.expired_leave_project(self.project, self.user)
else
event_service.leave_project(self.project, self.user)
end
super
end
......
......@@ -137,6 +137,10 @@ class MergeRequest < ActiveRecord::Base
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end
def self.project_foreign_key
'target_project_id'
end
# Returns all the merge requests from an ActiveRecord:Relation.
#
# This method uses a UNION as it usually operates on the result of
......@@ -787,21 +791,21 @@ class MergeRequest < ActiveRecord::Base
def all_pipelines
return unless source_project
@all_pipelines ||= begin
sha = if persisted?
all_commits_sha
else
diff_head_sha
end
source_project.pipelines.order(id: :desc).
where(sha: sha, ref: source_branch)
end
@all_pipelines ||= source_project.pipelines
.where(sha: all_commits_sha, ref: source_branch)
.order(id: :desc)
end
# Note that this could also return SHA from now dangling commits
#
def all_commits_sha
merge_request_diffs.flat_map(&:commits_sha).uniq
if persisted?
merge_request_diffs.flat_map(&:commits_sha).uniq
elsif compare_commits
compare_commits.to_a.reverse.map(&:id)
else
[diff_head_sha]
end
end
def merge_commit
......
......@@ -107,7 +107,7 @@ class Project < ActiveRecord::Base
# 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 :issues, dependent: :destroy
has_many :labels, dependent: :destroy
has_many :labels, dependent: :destroy, class_name: 'ProjectLabel'
has_many :services, dependent: :destroy
has_many :events, dependent: :destroy
has_many :milestones, dependent: :destroy
......@@ -388,6 +388,10 @@ class Project < ActiveRecord::Base
Project.count
end
end
def group_ids
joins(:namespace).where(namespaces: { type: 'Group' }).pluck(:namespace_id)
end
end
def lfs_enabled?
......@@ -664,6 +668,10 @@ class Project < ActiveRecord::Base
end
end
def issue_reference_pattern
issues_tracker.reference_pattern
end
def default_issues_tracker?
!external_issue_tracker
end
......@@ -729,10 +737,8 @@ class Project < ActiveRecord::Base
def create_labels
Label.templates.each do |label|
label = label.dup
label.template = nil
label.project_id = self.id
label.save
params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
Labels::FindOrCreateService.new(owner, self, params).execute
end
end
......@@ -1293,7 +1299,7 @@ class Project < ActiveRecord::Base
environment_ids.where(ref: ref)
end
environments.where(id: environment_ids).select do |environment|
environments.available.where(id: environment_ids).select do |environment|
environment.includes_commit?(commit)
end
end
......
......@@ -13,23 +13,26 @@ class ProjectFeature < ActiveRecord::Base
# Enabled: enabled for everyone able to access the project
#
# Permision levels
# Permission levels
DISABLED = 0
PRIVATE = 10
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
# permissions for a project in pending_delete
# http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
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 :issues_access_level, value: ENABLED, allows_nil: false
default_value_for :merge_requests_access_level, value: ENABLED, allows_nil: false
default_value_for :snippets_access_level, value: ENABLED, allows_nil: false
default_value_for :wiki_access_level, value: ENABLED, allows_nil: false
default_value_for :repository_access_level, value: ENABLED, allows_nil: false
def feature_available?(feature, user)
raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature)
......@@ -57,6 +60,18 @@ class ProjectFeature < ActiveRecord::Base
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)
case level
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
......@@ -3,6 +3,12 @@ class IssueTrackerService < Service
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?
default
end
......
......@@ -13,6 +13,11 @@ class JiraService < IssueTrackerService
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
# don't reset the password if a new one is provided
if api_url_changed? && !password_touched?
......
......@@ -52,7 +52,13 @@ class Todo < ActiveRecord::Base
# 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"
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").
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
if master
can! :create_projects
can! :admin_milestones
can! :admin_label
end
# 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
end
def disabled_features!
repository_enabled = project.feature_available?(:repository, user)
unless project.feature_available?(:issues, user)
cannot!(*named_abilities(:issue))
end
unless project.feature_available?(:merge_requests, user)
unless project.feature_available?(:merge_requests, user) && repository_enabled
cannot!(*named_abilities(:merge_request))
end
......@@ -183,13 +185,21 @@ class ProjectPolicy < BasePolicy
cannot!(*named_abilities(:wiki))
end
unless project.feature_available?(:builds, user)
unless project.feature_available?(:builds, user) && repository_enabled
cannot!(*named_abilities(:build))
cannot!(*named_abilities(:pipeline))
cannot!(*named_abilities(:environment))
cannot!(*named_abilities(:deployment))
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
cannot!(*named_abilities(:container_image))
end
......
......@@ -3,7 +3,7 @@ module Boards
class CreateService < BaseService
def execute(board)
List.transaction do
label = project.labels.find(params[:label_id])
label = available_labels.find(params[:label_id])
position = next_position(board)
create_list(board, label, position)
......@@ -12,6 +12,10 @@ module Boards
private
def available_labels
LabelsFinder.new(current_user, project_id: project.id).execute
end
def next_position(board)
max_position = board.lists.movable.maximum(:position)
max_position.nil? ? 0 : max_position.succ
......
......@@ -19,8 +19,7 @@ module Boards
end
def find_or_create_label(params)
project.labels.create_with(color: params[:color])
.find_or_create_by(name: params[:name])
::Labels::FindOrCreateService.new(current_user, project, params).execute
end
def label_params
......
......@@ -6,7 +6,13 @@ class CreateDeploymentService < BaseService
ActiveRecord::Base.transaction do
@deployable = deployable
@environment = prepare_environment
@environment = environment
@environment.external_url = expanded_url if expanded_url
@environment.fire_state_event(action)
return unless @environment.save
return if @environment.stopped?
deploy.tap do |deployment|
deployment.update_merge_request_metrics!
......@@ -27,13 +33,12 @@ class CreateDeploymentService < BaseService
tag: params[:tag],
sha: params[:sha],
user: current_user,
deployable: @deployable)
deployable: @deployable,
on_stop: options[:on_stop])
end
def prepare_environment
project.environments.find_or_create_by(name: expanded_name) do |environment|
environment.external_url = expanded_url
end
def environment
@environment ||= project.environments.find_or_create_by(name: expanded_name)
end
def expanded_name
......@@ -61,4 +66,8 @@ class CreateDeploymentService < BaseService
def variables
params[:variables] || []
end
def action
options[:action] || 'start'
end
end
......@@ -62,6 +62,10 @@ class EventCreateService
create_event(project, current_user, Event::LEFT)
end
def expired_leave_project(project, current_user)
create_event(project, current_user, Event::EXPIRED)
end
def create_project(project, current_user)
create_event(project, current_user, Event::CREATED)
end
......
......@@ -80,17 +80,18 @@ class IssuableBaseService < BaseService
def filter_labels_in_param(key)
return if params[key].to_a.empty?
params[key] = project.labels.where(id: params[key]).pluck(:id)
params[key] = available_labels.where(id: params[key]).pluck(:id)
end
def find_or_create_label_ids
labels = params.delete(:labels)
return unless labels
params[:label_ids] = labels.split(",").map do |label_name|
project.labels.create_with(color: Label::DEFAULT_COLOR)
.find_or_create_by(title: label_name.strip)
.id
params[:label_ids] = labels.split(',').map do |label_name|
service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip)
label = service.execute
label.id
end
end
......@@ -111,6 +112,10 @@ class IssuableBaseService < BaseService
new_label_ids
end
def available_labels
LabelsFinder.new(current_user, project_id: @project.id).execute
end
def merge_slash_commands_into_params!(issuable)
description, command_params =
SlashCommands::InterpretService.new(project, current_user).
......
......@@ -52,8 +52,12 @@ module Issues
end
def cloneable_label_ids
@new_project.labels
.where(title: @old_issue.labels.pluck(:title)).pluck(:id)
params = {
project_id: @new_project.id,
title: @old_issue.labels.pluck(:title)
}
LabelsFinder.new(current_user, params).execute.pluck(:id)
end
def cloneable_milestone_id
......
module Labels
class FindOrCreateService
def initialize(current_user, project, params = {})
@current_user = current_user
@group = project.group
@project = project
@params = params.dup
end
def execute
find_or_create_label
end
private
attr_reader :current_user, :group, :project, :params
def available_labels
@available_labels ||= LabelsFinder.new(current_user, project_id: project.id).execute
end
def find_or_create_label
new_label = available_labels.find_by(title: title)
new_label ||= project.labels.create(params)
new_label
end
def title
params[:title] || params[:name]
end
end
end
# Labels::TransferService class
#
# User for recreate the missing group labels at project level
#
module Labels
class TransferService
def initialize(current_user, old_group, project)
@current_user = current_user
@old_group = old_group
@project = project
end
def execute
return unless old_group.present?
Label.transaction do
labels_to_transfer.find_each do |label|
new_label_id = find_or_create_label!(label)
next if new_label_id == label.id
update_label_links(group_labels_applied_to_issues, old_label_id: label.id, new_label_id: new_label_id)
update_label_links(group_labels_applied_to_merge_requests, old_label_id: label.id, new_label_id: new_label_id)
update_label_priorities(old_label_id: label.id, new_label_id: new_label_id)
end
end
end
private
attr_reader :current_user, :old_group, :project
def labels_to_transfer
label_ids = []
label_ids << group_labels_applied_to_issues.select(:id)
label_ids << group_labels_applied_to_merge_requests.select(:id)
union = Gitlab::SQL::Union.new(label_ids)
Label.where("labels.id IN (#{union.to_sql})").reorder(nil).uniq
end
def group_labels_applied_to_issues
Label.joins(:issues).
where(
issues: { project_id: project.id },
labels: { type: 'GroupLabel', group_id: old_group.id }
)
end
def group_labels_applied_to_merge_requests
Label.joins(:merge_requests).
where(
merge_requests: { target_project_id: project.id },
labels: { type: 'GroupLabel', group_id: old_group.id }
)
end
def find_or_create_label!(label)
params = label.attributes.slice('title', 'description', 'color')
new_label = FindOrCreateService.new(current_user, project, params).execute
new_label.id
end
def update_label_links(labels, old_label_id:, new_label_id:)
LabelLink.joins(:label).
merge(labels).
where(label_id: old_label_id).
update_all(label_id: new_label_id)
end
def update_label_priorities(old_label_id:, new_label_id:)
LabelPriority.where(project_id: project.id, label_id: old_label_id).
update_all(label_id: new_label_id)
end
end
end
......@@ -11,14 +11,14 @@ module MergeRequests
def execute(merge_request)
@merge_request = merge_request
return error('Merge request is not mergeable') unless @merge_request.mergeable?
return log_merge_error('Merge request is not mergeable', true) unless @merge_request.mergeable?
merge_request.in_locked_state do
if commit
after_merge
success
else
error('Can not merge changes')
log_merge_error('Can not merge changes', true)
end
end
end
......@@ -46,8 +46,8 @@ module MergeRequests
merge_request.update(merge_error: e.message)
false
rescue StandardError => e
merge_request.update(merge_error: "Something went wrong during merge")
Rails.logger.error(e.message)
merge_request.update(merge_error: "Something went wrong during merge: #{e.message}")
log_merge_error(e.message)
false
ensure
merge_request.update(in_progress_merge_commit_sha: nil)
......@@ -65,5 +65,17 @@ module MergeRequests
def branch_deletion_user
@merge_request.force_remove_source_branch? ? @merge_request.author : current_user
end
def log_merge_error(message, http_error = false)
Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{message}")
error(message) if http_error
end
def merge_request_info
project = merge_request.project
"#{project.to_reference}#{merge_request.to_reference}"
end
end
end
......@@ -13,7 +13,7 @@ module Projects
end
def labels
@project.labels.select([:title, :color])
LabelsFinder.new(current_user, project_id: project.id).execute.select([:title, :color])
end
def commands(noteable, type)
......
......@@ -28,6 +28,7 @@ module Projects
Project.transaction do
old_path = project.path_with_namespace
old_namespace = project.namespace
old_group = project.group
new_path = File.join(new_namespace.try(:path) || '', project.path)
if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present?
......@@ -57,6 +58,9 @@ module Projects
# Move wiki repo also if present
gitlab_shell.mv_repository(project.repository_storage_path, "#{old_path}.wiki", "#{new_path}.wiki")
# Move missing group labels to project
Labels::TransferService.new(current_user, old_group, project).execute
# clear project cached events
project.reset_events_cache
......
......@@ -116,8 +116,10 @@ module SlashCommands
desc 'Add label(s)'
params '~label1 ~"label 2"'
condition do
available_labels = LabelsFinder.new(current_user, project_id: project.id).execute
current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
project.labels.any?
available_labels.any?
end
command :label do |labels_param|
label_ids = find_label_ids(labels_param)
......@@ -248,7 +250,7 @@ module SlashCommands
def find_label_ids(labels_param)
label_ids_by_reference = extract_references(labels_param, :label).map(&:id)
labels_ids_by_name = @project.labels.where(name: labels_param.split).select(:id)
labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id)
label_ids_by_reference | labels_ids_by_name
end
......
= form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user', class: 'show-gl-field-errors') do
.form-group
= label_tag 'Username or email', for: :username
= label_tag :username, 'Username or email'
= text_field_tag :username, nil, {class: "form-control top", title: "This field is required", autofocus: "autofocus", required: true }
.form-group
= label_tag :password
......@@ -10,4 +10,4 @@
%label{for: "remember_me"}
= check_box_tag :remember_me, '1', false, id: 'remember_me'
%span Remember me
= button_tag "Sign in", class: "btn-save btn"
= submit_tag "Sign in", class: "btn-save btn"
= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "show-gl-field-errors") do
.form-group
= label_tag "#{server['label']} Login", for: :username
= label_tag :username, "#{server['label']} Username"
= text_field_tag :username, nil, {class: "form-control top", title: "This field is required.", autofocus: "autofocus", required: true }
.form-group
= label_tag :password
......@@ -10,4 +10,4 @@
%label{for: "remember_me"}
= check_box_tag :remember_me, '1', false, id: 'remember_me'
%span Remember me
= button_tag "Sign in", class: "btn-save btn"
= submit_tag "Sign in", class: "btn-save btn"
#login-pane.login-box{ role: 'tabpanel', class: 'tab-pane active' }
.login-body
- if form_based_providers.any?
- if crowd_enabled?
%div.tab-pane.active{id: "tab-crowd"}
= render 'devise/sessions/new_crowd'
- @ldap_servers.each_with_index do |server, i|
%div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i.zero? && !crowd_enabled?)}
= render 'devise/sessions/new_ldap', server: server
- if signin_enabled?
%div#tab-signin.tab-pane
= render 'devise/sessions/new_base'
- if form_based_providers.any?
- if crowd_enabled?
.login-box.tab-pane.active{id: "crowd", role: 'tabpanel', class: 'tab-pane'}
.login-body
= render 'devise/sessions/new_crowd'
- @ldap_servers.each_with_index do |server, i|
.login-box.tab-pane{id: "#{server['provider_name']}", role: 'tabpanel', class: (:active if i.zero? && !crowd_enabled?)}
.login-body
= render 'devise/sessions/new_ldap', server: server
- if signin_enabled?
.login-box.tab-pane{id: 'ldap-standard', role: 'tabpanel'}
.login-body
= render 'devise/sessions/new_base'
- elsif signin_enabled?
- elsif signin_enabled?
.login-box.tab-pane.active{id: 'login-pane', role: 'tabpanel'}
.login-body
= render 'devise/sessions/new_base'
%ul.new-session-tabs.nav-links.nav-tabs
- if crowd_enabled?
%li.active
= link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab'
= link_to "Crowd", "#crowd", 'data-toggle' => 'tab'
- @ldap_servers.each_with_index do |server, i|
%li{class: (:active if i.zero? && !crowd_enabled?)}
= link_to server['label'], "#tab-#{server['provider_name']}", 'data-toggle' => 'tab'
= link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab'
- if signin_enabled?
%li
= link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab'
= link_to 'Standard', '#ldap-standard', 'data-toggle' => 'tab'
- if @group.labels.empty?
$('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
- page_title 'Edit', @label.name, 'Labels'
%h3.page-title
Edit Label
%hr
= render 'shared/labels/form', url: group_label_path(@group, @label), back_path: @previous_labels_path
- page_title 'Labels'
.top-area.adjust
.nav-text
Labels can be applied to issues and merge requests. Group labels are available for any project within the group.
.nav-controls
- if can?(current_user, :admin_label, @group)
= link_to new_group_label_path(@group), class: "btn btn-new" do
New label
.labels
.other-labels
- if @labels.present?
%ul.content-list.manage-labels-list.js-other-labels
= render partial: 'shared/label', collection: @labels, as: :label
= paginate @labels, theme: 'gitlab'
- else
.nothing-here-block
No labels created yet.
- page_title 'New Label'
- header_title group_title(@group, 'Labels', group_labels_path(@group))
%h3.page-title
New Label
%hr
= render 'shared/labels/form', url: group_labels_path, back_path: @previous_labels_path
......@@ -13,6 +13,10 @@
= link_to activity_group_path(@group), title: 'Activity' do
%span
Activity
= nav_link(controller: [:group, :labels]) do
= link_to group_labels_path(@group), title: 'Labels' do
%span
Labels
= nav_link(controller: [:group, :milestones]) do
= link_to group_milestones_path(@group), title: 'Milestones' do
%span
......
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
%html{lang: "en"}
%head
%meta{content: "text/html; charset=UTF-8", "http-equiv": "Content-Type"}/
%meta{content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
%meta{content: "width=device-width, initial-scale=1", name: "viewport"}/
%meta{content: "IE=edge", "http-equiv": "X-UA-Compatible"}/
%meta{content: "IE=edge", "http-equiv" => "X-UA-Compatible"}/
%title= message.subject
:css
/* CLIENT-SPECIFIC STYLES */
......
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional //EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
%html{lang: "en"}
%head
%meta{content: "text/html; charset=UTF-8", "http-equiv": "Content-Type"}/
%meta{content: "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}/
%meta{content: "width=device-width, initial-scale=1", name: "viewport"}/
%meta{content: "IE=edge", "http-equiv": "X-UA-Compatible"}/
%meta{content: "IE=edge", "http-equiv" => "X-UA-Compatible"}/
%title= message.subject
:css
/* CLIENT-SPECIFIC STYLES */
......
.row-content-block.project-home-empty
%div.text-center{ class: container_class }
%h4
Customize your workflow!
%p
Get started with GitLab by enabling features that work best for your project. From issues and wikis, to merge requests and builds, GitLab can help manage your workflow from idea to production!
- if can?(current_user, :admin_project, @project)
= link_to "Get started", edit_project_path(@project), class: "btn btn-success"
......@@ -22,5 +22,6 @@
= render 'projects/buttons/star'
= render 'projects/buttons/fork'
.project-clone-holder
= render "shared/clone_panel"
- if @project.feature_available?(:repository, current_user)
.project-clone-holder
= render "shared/clone_panel"
- if @wiki_home.present?
%div{ class: container_class }
.wiki-holder.prepend-top-default.append-bottom-default
.wiki
= preserve do
= render_wiki_content(@wiki_home)
- else
- can_create_wiki = can?(current_user, :create_wiki, @project)
.project-home-empty{ class: [('row-content-block' if can_create_wiki), ('content-block' unless can_create_wiki)] }
%div.text-center{ class: container_class }
%h4
This project does not have a wiki homepage yet
- if can_create_wiki
%p
Add a homepage to your wiki that contains information about your project
%p
We recommend you
= link_to "add a homepage", namespace_project_wiki_path(@project.namespace, @project, :home)
to your project's wiki and GitLab will show it here instead of this message.
- builds = @build.pipeline.builds.latest.to_a
- statuses = ["failed", "pending", "running", "canceled", "success", "skipped"]
- builds = @build.pipeline.builds.to_a
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar
.block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
......@@ -124,9 +123,9 @@
%a.stage-item= stage
.builds-container
- statuses.each do |build_status|
- HasStatus::ORDERED_STATUSES.each do |build_status|
- builds.select{|build| build.status == build_status}.each do |build|
.build-job{class: ('active' if build == @build), data: {stage: build.stage}}
.build-job{class: sidebar_build_class(build, @build), data: {stage: build.stage}}
= link_to namespace_project_build_path(@project.namespace, @project, build) do
= icon('arrow-right')
= ci_icon_for_status(build.status)
......@@ -135,11 +134,5 @@
= build.name
- else
= build.id
- if @build.retried?
%li.active
%a
Build ##{@build.id}
&middot;
%i.fa.fa-warning
This build was retried.
- if build.retried?
%i.fa.fa-refresh.has-tooltip{data: { container: 'body', placement: 'bottom' }, title: 'Build was retried'}
by
%a{ href: user_path(@build.user) }
= image_tag avatar_icon(@build.user, 24), class: "avatar s24"
%strong= @build.user.to_reference
%span.hidden-xs
= image_tag avatar_icon(@build.user, 24), class: "avatar s24"
%strong{ data: { toggle: 'tooltip', placement: 'top', title: @build.user.to_reference } }
= @build.user.name
%strong.visible-xs-inline= @build.user.to_reference
......@@ -6,7 +6,7 @@
- coverage = local_assigns.fetch(:coverage, false)
- allow_retry = local_assigns.fetch(:allow_retry, false)
%tr.build.commit
%tr.build.commit{class: ('retried' if retried)}
%td.status
- if can?(current_user, :read_build, build)
= ci_status_with_icon(build.status, namespace_project_build_url(build.project.namespace, build.project, build))
......@@ -27,7 +27,7 @@
= link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name"
- else
.light none
.icon-container
.icon-container.commit-icon
= custom_icon("icon_commit")
- if commit_sha
......@@ -35,8 +35,9 @@
- if build.stuck?
= icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.')
- if retried
= icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.')
= icon('refresh', class: 'text-warning has-tooltip', title: 'Build was retried')
.label-container
- if build.tags.any?
......@@ -47,8 +48,6 @@
%span.label.label-info triggered
- if build.try(:allow_failure)
%span.label.label-danger allowed to fail
- if retried
%span.label.label-warning retried
- if build.manual?
%span.label.label-info manual
......
......@@ -9,17 +9,15 @@
= ci_icon_for_status(status)
- else
= ci_status_with_icon(status)
%td.branch-commit
%td
= link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
%span ##{pipeline.id}
- if pipeline.ref && show_branch
.icon-container
= pipeline.tag? ? icon('tag') : icon('code-fork')
= link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name"
- if show_commit
.icon-container
= custom_icon("icon_commit")
= link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace"
%span.pipeline-id ##{pipeline.id}
%span by
- if pipeline.user
= user_avatar(user: pipeline.user, size: 20)
- else
%span.api.monospace API
- if pipeline.latest?
%span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest
- if pipeline.triggered?
......@@ -29,6 +27,16 @@
- if pipeline.builds.any?(&:stuck?)
%span.label.label-warning stuck
%td.branch-commit
- if pipeline.ref && show_branch
.icon-container
= pipeline.tag? ? icon('tag') : icon('code-fork')
= link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name"
- if show_commit
.icon-container.commit-icon
= custom_icon("icon_commit")
= link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace"
%p.commit-title
- if commit = pipeline.commit
= author_avatar(commit, size: 20)
......@@ -36,16 +44,15 @@
- else
Cant find HEAD commit for this branch
- stages_status = pipeline.statuses.relevant.latest.stages_status
%td.stage-cell
- stages.each do |stage|
- status = stages_status[stage]
- tooltip = "#{stage.titleize}: #{status || 'not found'}"
- if status
.stage-container
= link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do
= ci_icon_for_status(status)
- stages_status = pipeline.statuses.relevant.latest.stages_status
%td.stage-cell
- stages.each do |stage|
- status = stages_status[stage]
- tooltip = "#{stage.titleize}: #{status || 'not found'}"
- if status
.stage-container
= link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do
= ci_icon_for_status(status)
%td
- if pipeline.duration
......
......@@ -8,6 +8,7 @@
%tbody
%th Status
%th Pipeline
%th Commit
%th Stages
%th
%th
......
......@@ -2,10 +2,10 @@
- page_title "Cycle Analytics"
= render "projects/pipelines/head"
#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project)}}
#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) }}
.bordered-box.landing.content-block{"v-if" => "!isHelpDismissed"}
= icon('times', class: 'dismiss-icon', "@click": "dismissLanding()")
= icon('times', class: 'dismiss-icon', "@click" => "dismissLanding()")
.row
.col-sm-3.col-xs-12.svg-container
= custom_icon('icon_cycle_analytics_splash')
......
- if can?(current_user, :create_deployment, deployment) && deployment.deployable
.pull-right
- external_url = deployment.environment.external_url
- if external_url
= link_to external_url, target: '_blank', class: 'btn external-url' do
= icon('external-link')
- actions = deployment.manual_actions
- if actions.present?
.inline
.dropdown
%a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
= custom_icon('icon_play')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |action|
%li
= link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
= custom_icon('icon_play')
%span= action.name.humanize
- if can?(current_user, :create_deployment, deployment)
- actions = deployment.manual_actions
- if actions.present?
.inline
.dropdown
%a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
= custom_icon('icon_play')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |action|
%li
= link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
= custom_icon('icon_play')
%span= action.name.humanize
- if local_assigns.fetch(:allow_rollback, false)
= link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do
- if deployment.last?
Re-deploy
- else
Rollback
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