Commit 4ab27ca5 authored by Alejandro Rodríguez's avatar Alejandro Rodríguez

Merge remote-tracking branch 'ce/master' into ce-upstream

parents 516c323d 149df275
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-undef, quotes, no-var, padded-blocks, max-len */
(function() {
this.Activities = (function() {
function Activities() {
Pager.init(20, true, false, this.updateTooltips);
$(".event-filter-link").on("click", (function(_this) {
return function(event) {
event.preventDefault();
_this.toggleFilter($(event.currentTarget));
return _this.reloadActivities();
};
})(this));
}
Activities.prototype.updateTooltips = function() {
gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
};
Activities.prototype.reloadActivities = function() {
$(".content_list").html('');
Pager.init(20, true, false, this.updateTooltips);
};
Activities.prototype.toggleFilter = function(sender) {
var filter = sender.attr("id").split("_")[0];
$('.event-filter .active').removeClass("active");
Cookies.set("event_filter", filter);
sender.closest('li').toggleClass("active");
};
return Activities;
})();
}).call(this);
/* eslint-disable no-param-reassign, class-methods-use-this */
/* global Pager, Cookies */
((global) => {
class Activities {
constructor() {
Pager.init(20, true, false, this.updateTooltips);
$('.event-filter-link').on('click', (e) => {
e.preventDefault();
this.toggleFilter(e.currentTarget);
this.reloadActivities();
});
}
updateTooltips() {
gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
}
reloadActivities() {
$('.content_list').html('');
Pager.init(20, true, false, this.updateTooltips);
}
toggleFilter(sender) {
const $sender = $(sender);
const filter = $sender.attr('id').split('_')[0];
$('.event-filter .active').removeClass('active');
Cookies.set('event_filter', filter);
$sender.closest('li').toggleClass('active');
}
}
global.Activities = Activities;
})(window.gl || (window.gl = {}));
...@@ -111,10 +111,10 @@ ...@@ -111,10 +111,10 @@
Issuable.init(); Issuable.init();
break; break;
case 'dashboard:activity': case 'dashboard:activity':
new Activities(); new gl.Activities();
break; break;
case 'dashboard:projects:starred': case 'dashboard:projects:starred':
new Activities(); new gl.Activities();
break; break;
case 'projects:commit:show': case 'projects:commit:show':
new Commit(); new Commit();
...@@ -140,7 +140,7 @@ ...@@ -140,7 +140,7 @@
new gl.Pipelines(); new gl.Pipelines();
break; break;
case 'groups:activity': case 'groups:activity':
new Activities(); new gl.Activities();
break; break;
case 'groups:show': case 'groups:show':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
......
...@@ -157,17 +157,17 @@ ...@@ -157,17 +157,17 @@
<li v-bind:class="{ 'active': scope === undefined }"> <li v-bind:class="{ 'active': scope === undefined }">
<a :href="projectEnvironmentsPath"> <a :href="projectEnvironmentsPath">
Available Available
<span <span class="badge js-available-environments-count">
class="badge js-available-environments-count" {{state.availableCounter}}
v-html="state.availableCounter"></span> </span>
</a> </a>
</li> </li>
<li v-bind:class="{ 'active' : scope === 'stopped' }"> <li v-bind:class="{ 'active' : scope === 'stopped' }">
<a :href="projectStoppedEnvironmentsPath"> <a :href="projectStoppedEnvironmentsPath">
Stopped Stopped
<span <span class="badge js-stopped-environments-count">
class="badge js-stopped-environments-count" {{state.stoppedCounter}}
v-html="state.stoppedCounter"></span> </span>
</a> </a>
</li> </li>
</ul> </ul>
...@@ -183,8 +183,7 @@ ...@@ -183,8 +183,7 @@
<i class="fa fa-spinner spin"></i> <i class="fa fa-spinner spin"></i>
</div> </div>
<div <div class="blank-state blank-state-no-icon"
class="blank-state blank-state-no-icon"
v-if="!isLoading && state.environments.length === 0"> v-if="!isLoading && state.environments.length === 0">
<h2 class="blank-state-title"> <h2 class="blank-state-title">
You don't have any environments right now. You don't have any environments right now.
...@@ -205,8 +204,7 @@ ...@@ -205,8 +204,7 @@
</a> </a>
</div> </div>
<div <div class="table-holder"
class="table-holder"
v-if="!isLoading && state.environments.length > 0"> v-if="!isLoading && state.environments.length > 0">
<table class="table ci-table environments"> <table class="table ci-table environments">
<thead> <thead>
...@@ -234,7 +232,9 @@ ...@@ -234,7 +232,9 @@
is="environment-item" is="environment-item"
v-for="children in model.children" v-for="children in model.children"
:model="children" :model="children"
:toggleRow="toggleRow.bind(children)"> :toggleRow="toggleRow.bind(children)"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed">
</tr> </tr>
</template> </template>
......
...@@ -43,8 +43,7 @@ ...@@ -43,8 +43,7 @@
<div class="inline"> <div class="inline">
<div class="dropdown"> <div class="dropdown">
<a class="dropdown-new btn btn-default" data-toggle="dropdown"> <a class="dropdown-new btn btn-default" data-toggle="dropdown">
<span class="dropdown-play-icon-container"> <span class="dropdown-play-icon-container"></span>
</span>
<i class="fa fa-caret-down"></i> <i class="fa fa-caret-down"></i>
</a> </a>
...@@ -54,9 +53,10 @@ ...@@ -54,9 +53,10 @@
data-method="post" data-method="post"
rel="nofollow" rel="nofollow"
class="js-manual-action-link"> class="js-manual-action-link">
<span class="action-play-icon-container"> <span class="action-play-icon-container"></span>
<span>
{{action.name}}
</span> </span>
<span v-html="action.name"></span>
</a> </a>
</li> </li>
</ul> </ul>
......
...@@ -389,11 +389,10 @@ ...@@ -389,11 +389,10 @@
template: ` template: `
<tr> <tr>
<td v-bind:class="{ 'children-row': isChildren}"> <td v-bind:class="{ 'children-row': isChildren}">
<a <a v-if="!isFolder"
v-if="!isFolder"
class="environment-name" class="environment-name"
:href="model.environment_path" :href="model.environment_path">
v-html="model.name"> {{model.name}}
</a> </a>
<span v-else v-on:click="toggleRow(model)" class="folder-name"> <span v-else v-on:click="toggleRow(model)" class="folder-name">
<span class="folder-icon"> <span class="folder-icon">
...@@ -401,16 +400,19 @@ ...@@ -401,16 +400,19 @@
<i v-show="!model.isOpen" class="fa fa-caret-right"></i> <i v-show="!model.isOpen" class="fa fa-caret-right"></i>
</span> </span>
<span v-html="model.name"></span> <span>
{{model.name}}
</span>
<span class="badge" v-html="childrenCounter"></span> <span class="badge">
{{childrenCounter}}
</span>
</span> </span>
</td> </td>
<td class="deployment-column"> <td class="deployment-column">
<span <span v-if="shouldRenderDeploymentID">
v-if="shouldRenderDeploymentID" {{deploymentInternalId}}
v-html="deploymentInternalId">
</span> </span>
<span v-if="!isFolder && deploymentHasUser"> <span v-if="!isFolder && deploymentHasUser">
...@@ -427,8 +429,8 @@ ...@@ -427,8 +429,8 @@
<td> <td>
<a v-if="shouldRenderBuildName" <a v-if="shouldRenderBuildName"
class="build-link" class="build-link"
:href="model.last_deployment.deployable.build_path" :href="model.last_deployment.deployable.build_path">
v-html="buildName"> {{buildName}}
</a> </a>
</td> </td>
...@@ -451,8 +453,8 @@ ...@@ -451,8 +453,8 @@
<td> <td>
<span <span
v-if="!isFolder && model.last_deployment" v-if="!isFolder && model.last_deployment"
class="environment-created-date-timeago" class="environment-created-date-timeago">
v-html="createdDate"> {{createdDate}}
</span> </span>
</td> </td>
......
...@@ -14,8 +14,7 @@ ...@@ -14,8 +14,7 @@
}, },
template: ` template: `
<a <a class="btn stop-env-link"
class="btn stop-env-link"
:href="stop_url" :href="stop_url"
data-confirm="Are you sure you want to stop this environment?" data-confirm="Are you sure you want to stop this environment?"
data-method="post" data-method="post"
......
...@@ -235,7 +235,7 @@ ...@@ -235,7 +235,7 @@
} }
if (environment.deployed_at && environment.deployed_at_formatted) { if (environment.deployed_at && environment.deployed_at_formatted) {
environment.deployed_at = gl.utils.getTimeago(environment.deployed_at) + '.'; environment.deployed_at = gl.utils.getTimeago().format(environment.deployed_at, 'gl_en') + '.';
} else { } else {
$('.js-environment-timeago', $template).remove(); $('.js-environment-timeago', $template).remove();
environment.name += '.'; environment.name += '.';
......
/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-undef, prefer-template, wrap-iife, comma-dangle, no-return-assign, no-else-return, consistent-return, no-unused-vars, padded-blocks, max-len */
(function() {
this.Pager = {
init: function(limit, preload, disable, callback) {
this.limit = limit != null ? limit : 0;
this.disable = disable != null ? disable : false;
this.callback = callback != null ? callback : $.noop;
this.loading = $('.loading').first();
if (preload) {
this.offset = 0;
this.getOld();
} else {
this.offset = this.limit;
}
return this.initLoadMore();
},
getOld: function() {
this.loading.show();
return $.ajax({
type: "GET",
url: $(".content_list").data('href') || location.href,
data: "limit=" + this.limit + "&offset=" + this.offset,
complete: (function(_this) {
return function() {
return _this.loading.hide();
};
})(this),
success: function(data) {
Pager.append(data.count, data.html);
return Pager.callback();
},
dataType: "json"
});
},
append: function(count, html) {
$(".content_list").append(html);
if (count > 0) {
return this.offset += count;
} else {
return this.disable = true;
}
},
initLoadMore: function() {
$(document).unbind('scroll');
return $(document).endlessScroll({
bottomPixels: 400,
fireDelay: 1000,
fireOnce: true,
ceaseFire: function() {
return Pager.disable;
},
callback: (function(_this) {
return function(i) {
if (!_this.loading.is(':visible')) {
_this.loading.show();
return Pager.getOld();
}
};
})(this)
});
}
};
}).call(this);
(() => {
const ENDLESS_SCROLL_BOTTOM_PX = 400;
const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000;
const Pager = {
init(limit = 0, preload = false, disable = false, callback = $.noop) {
this.limit = limit;
this.offset = this.limit;
this.disable = disable;
this.callback = callback;
this.loading = $('.loading').first();
if (preload) {
this.offset = 0;
this.getOld();
}
this.initLoadMore();
},
getOld() {
this.loading.show();
$.ajax({
type: 'GET',
url: $('.content_list').data('href') || window.location.href,
data: `limit=${this.limit}&offset=${this.offset}`,
dataType: 'json',
error: () => this.loading.hide(),
success: (data) => {
this.append(data.count, data.html);
this.callback();
// keep loading until we've filled the viewport height
if (!this.disable && !this.isScrollable()) {
this.getOld();
} else {
this.loading.hide();
}
},
});
},
append(count, html) {
$('.content_list').append(html);
if (count > 0) {
this.offset += count;
} else {
this.disable = true;
}
},
isScrollable() {
const $w = $(window);
return $(document).height() > $w.height() + $w.scrollTop() + ENDLESS_SCROLL_BOTTOM_PX;
},
initLoadMore() {
$(document).unbind('scroll');
$(document).endlessScroll({
bottomPixels: ENDLESS_SCROLL_BOTTOM_PX,
fireDelay: ENDLESS_SCROLL_FIRE_DELAY_MS,
fireOnce: true,
ceaseFire: () => this.disable === true,
callback: () => {
if (!this.loading.is(':visible')) {
this.loading.show();
this.getOld();
}
},
});
},
};
window.Pager = Pager;
})();
...@@ -134,7 +134,7 @@ content on the Users#show page. ...@@ -134,7 +134,7 @@ content on the Users#show page.
} }
const $calendarWrap = this.$parentEl.find('.user-calendar'); const $calendarWrap = this.$parentEl.find('.user-calendar');
$calendarWrap.load($calendarWrap.data('href')); $calendarWrap.load($calendarWrap.data('href'));
new Activities(); new gl.Activities();
return this.loaded['activity'] = true; return this.loaded['activity'] = true;
} }
......
...@@ -138,16 +138,15 @@ ...@@ -138,16 +138,15 @@
<a v-if="hasRef" <a v-if="hasRef"
class="monospace branch-name" class="monospace branch-name"
:href="ref.ref_url" :href="ref.ref_url">
v-html="ref.name"> {{ref.name}}
</a> </a>
<div class="icon-container commit-icon commit-icon-container"> <div class="icon-container commit-icon commit-icon-container"></div>
</div>
<a class="commit-id monospace" <a class="commit-id monospace"
:href="commit_url" :href="commit_url">
v-html="short_sha"> {{short_sha}}
</a> </a>
<p class="commit-title"> <p class="commit-title">
...@@ -156,14 +155,15 @@ ...@@ -156,14 +155,15 @@
class="avatar-image-container" class="avatar-image-container"
:href="author.web_url"> :href="author.web_url">
<img <img
class="avatar has-tooltip s20" class="avatar has-tooltip s20"
:src="author.avatar_url" :src="author.avatar_url"
:alt="userImageAltDescription" :alt="userImageAltDescription"
:title="author.username" /> :title="author.username" />
</a> </a>
<a class="commit-row-message" <a class="commit-row-message"
:href="commit_url" v-html="title"> :href="commit_url">
{{title}}
</a> </a>
</span> </span>
<span v-else> <span v-else>
......
...@@ -255,26 +255,3 @@ ...@@ -255,26 +255,3 @@
} }
} }
// 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;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
.forgot-password {
float: none !important;
margin-top: 5px;
}
}
}
.move-submit-down {
position: absolute;
width: 100%;
bottom: 0;
}
}
...@@ -8,6 +8,10 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController ...@@ -8,6 +8,10 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
def show def show
@cycle_analytics = ::CycleAnalytics.new(@project, from: start_date(cycle_analytics_params)) @cycle_analytics = ::CycleAnalytics.new(@project, from: start_date(cycle_analytics_params))
stats_values, cycle_analytics_json = generate_cycle_analytics_data
@cycle_analytics_no_data = stats_values.blank?
respond_to do |format| respond_to do |format|
format.html format.html
format.json { render json: cycle_analytics_json } format.json { render json: cycle_analytics_json }
...@@ -22,7 +26,9 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController ...@@ -22,7 +26,9 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
{ start_date: params[:cycle_analytics][:start_date] } { start_date: params[:cycle_analytics][:start_date] }
end end
def cycle_analytics_json def generate_cycle_analytics_data
stats_values = []
cycle_analytics_view_data = [[:issue, "Issue", "Time before an issue gets scheduled"], cycle_analytics_view_data = [[:issue, "Issue", "Time before an issue gets scheduled"],
[:plan, "Plan", "Time before an issue starts implementation"], [:plan, "Plan", "Time before an issue starts implementation"],
[:code, "Code", "Time until first merge request"], [:code, "Code", "Time until first merge request"],
...@@ -34,11 +40,14 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController ...@@ -34,11 +40,14 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_description)| stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_description)|
value = @cycle_analytics.send(stage_method).presence value = @cycle_analytics.send(stage_method).presence
stats_values << value.abs if value
stats << { stats << {
title: stage_text, title: stage_text,
description: stage_description, description: stage_description,
value: value && !value.zero? ? distance_of_time_in_words(value) : nil value: value && !value.zero? ? distance_of_time_in_words(value) : nil
} }
stats stats
end end
...@@ -52,9 +61,11 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController ...@@ -52,9 +61,11 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
{ title: "Deploy".pluralize(deploys), value: deploys } { title: "Deploy".pluralize(deploys), value: deploys }
] ]
{ cycle_analytics_hash = { summary: summary,
summary: summary, stats: stats,
stats: stats permissions: @cycle_analytics.permissions(user: current_user)
} }
[stats_values, cycle_analytics_hash]
end end
end end
...@@ -62,7 +62,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -62,7 +62,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
format.html { define_discussion_vars } format.html { define_discussion_vars }
format.json do format.json do
<<<<<<< HEAD
render json: MergeRequestSerializer.new.represent(@merge_request, type: :full) render json: MergeRequestSerializer.new.represent(@merge_request, type: :full)
=======
render json: MergeRequestSerializer.new.represent(@merge_request)
>>>>>>> ce/master
end end
format.patch do format.patch do
...@@ -84,12 +88,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -84,12 +88,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request_diff = @merge_request_diff =
if params[:diff_id] if params[:diff_id]
@merge_request.merge_request_diffs.find(params[:diff_id]) @merge_request.merge_request_diffs.viewable.find(params[:diff_id])
else else
@merge_request.merge_request_diff @merge_request.merge_request_diff
end end
@merge_request_diffs = @merge_request.merge_request_diffs.select_without_diff @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id } @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present? if params[:start_sha].present?
...@@ -442,7 +446,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -442,7 +446,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
response = { response = {
title: merge_request.title, title: merge_request.title,
sha: merge_request.diff_head_commit.short_id, sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha),
status: status, status: status,
coverage: coverage coverage: coverage
} }
...@@ -601,7 +605,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -601,7 +605,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_pipelines_vars def define_pipelines_vars
@pipelines = @merge_request.all_pipelines @pipelines = @merge_request.all_pipelines
if @pipelines.present? if @pipelines.present? && @merge_request.commits.present?
@pipeline = @pipelines.first @pipeline = @pipelines.first
@statuses = @pipeline.statuses.relevant @statuses = @pipeline.statuses.relevant
end end
......
...@@ -197,7 +197,10 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -197,7 +197,10 @@ class Projects::NotesController < Projects::ApplicationController
) )
end end
<<<<<<< HEAD
attrs[:commands_changes] = note.commands_changes unless attrs[:award] attrs[:commands_changes] = note.commands_changes unless attrs[:award]
=======
>>>>>>> ce/master
attrs attrs
end end
......
...@@ -17,6 +17,8 @@ module ServicesHelper ...@@ -17,6 +17,8 @@ module ServicesHelper
"Event will be triggered when a build status changes" "Event will be triggered when a build status changes"
when "wiki_page" when "wiki_page"
"Event will be triggered when a wiki page is created/updated" "Event will be triggered when a wiki page is created/updated"
when "commit"
"Event will be triggered when a commit is created/updated"
end end
end end
......
...@@ -70,7 +70,11 @@ module Ci ...@@ -70,7 +70,11 @@ module Ci
environment: build.environment, environment: build.environment,
status_event: 'enqueue' status_event: 'enqueue'
) )
MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
MergeRequests::AddTodoWhenBuildFailsService
.new(build.project, nil)
.close(new_build)
build.pipeline.mark_as_processable_after_stage(build.stage_idx) build.pipeline.mark_as_processable_after_stage(build.stage_idx)
new_build new_build
end end
...@@ -484,6 +488,10 @@ module Ci ...@@ -484,6 +488,10 @@ module Ci
] ]
end end
def credentials
Gitlab::Ci::Build::Credentials::Factory.new(self).create!
end
private private
def update_artifacts_size def update_artifacts_size
......
# == Mentionable concern # == Mentionable concern
# #
# Contains functionality related to objects that can mention Users, Issues, MergeRequests, or Commits by # Contains functionality related to objects that can mention Users, Issues, MergeRequests, Commits or Snippets by
# GFM references. # GFM references.
# #
# Used by Issue, Note, MergeRequest, and Commit. # Used by Issue, Note, MergeRequest, and Commit.
......
class CycleAnalytics class CycleAnalytics
STAGES = %i[issue plan code test review staging production].freeze
def initialize(project, from:) def initialize(project, from:)
@project = project @project = project
@from = from @from = from
...@@ -9,6 +11,10 @@ class CycleAnalytics ...@@ -9,6 +11,10 @@ class CycleAnalytics
@summary ||= Summary.new(@project, from: @from) @summary ||= Summary.new(@project, from: @from)
end end
def permissions(user:)
Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project)
end
def issue def issue
@fetcher.calculate_metric(:issue, @fetcher.calculate_metric(:issue,
Issue.arel_table[:created_at], Issue.arel_table[:created_at],
......
...@@ -19,7 +19,7 @@ class Environment < ActiveRecord::Base ...@@ -19,7 +19,7 @@ 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 delegate :stop_action, :manual_actions, to: :last_deployment, allow_nil: true
scope :available, -> { with_state(:available) } scope :available, -> { with_state(:available) }
scope :stopped, -> { with_state(:stopped) } scope :stopped, -> { with_state(:stopped) }
...@@ -99,4 +99,12 @@ class Environment < ActiveRecord::Base ...@@ -99,4 +99,12 @@ class Environment < ActiveRecord::Base
stop stop
stop_action.play(current_user) stop_action.play(current_user)
end end
def actions_for(environment)
return [] unless manual_actions
manual_actions.select do |action|
action.expanded_environment_name == environment
end
end
end end
...@@ -11,6 +11,9 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -11,6 +11,9 @@ class MergeRequestDiff < ActiveRecord::Base
belongs_to :merge_request belongs_to :merge_request
serialize :st_commits
serialize :st_diffs
state_machine :state, initial: :empty do state_machine :state, initial: :empty do
state :collected state :collected
state :overflow state :overflow
...@@ -22,8 +25,7 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -22,8 +25,7 @@ class MergeRequestDiff < ActiveRecord::Base
state :overflow_diff_lines_limit state :overflow_diff_lines_limit
end end
serialize :st_commits scope :viewable, -> { without_state(:empty) }
serialize :st_diffs
# All diff information is collected from repository after object is created. # All diff information is collected from repository after object is created.
# It allows you to override variables like head_commit_sha before getting diff. # It allows you to override variables like head_commit_sha before getting diff.
......
...@@ -1196,7 +1196,7 @@ class Project < ActiveRecord::Base ...@@ -1196,7 +1196,7 @@ class Project < ActiveRecord::Base
"refs/heads/#{branch}", "refs/heads/#{branch}",
force: true) force: true)
repository.copy_gitattributes(branch) repository.copy_gitattributes(branch)
repository.expire_avatar_cache(branch) repository.expire_avatar_cache
reload_default_branch reload_default_branch
end end
......
# == Schema Information
#
# Table name: services
#
# id :integer not null, primary key
# type :string(255)
# title :string(255)
# project_id :integer
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
# template :boolean default(FALSE)
# push_events :boolean default(TRUE)
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
# build_events :boolean default(FALSE), not null
#
class JiraService < IssueTrackerService class JiraService < IssueTrackerService
include Gitlab::Routing.url_helpers include Gitlab::Routing.url_helpers
...@@ -30,6 +9,10 @@ class JiraService < IssueTrackerService ...@@ -30,6 +9,10 @@ class JiraService < IssueTrackerService
before_update :reset_password before_update :reset_password
def supported_events
%w(commit merge_request)
end
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1 # {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
def reference_pattern def reference_pattern
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)} @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
...@@ -137,19 +120,17 @@ class JiraService < IssueTrackerService ...@@ -137,19 +120,17 @@ class JiraService < IssueTrackerService
end end
def create_cross_reference_note(mentioned, noteable, author) def create_cross_reference_note(mentioned, noteable, author)
jira_issue = jira_request { client.Issue.find(mentioned.id) } unless can_cross_reference?(noteable)
return "Events for #{noteable.model_name.plural.humanize(capitalize: false)} are disabled."
end
return false unless jira_issue.present? jira_issue = jira_request { client.Issue.find(mentioned.id) }
project = self.project return unless jira_issue.present?
noteable_name = noteable.class.name.underscore.downcase
noteable_id = if noteable.is_a?(Commit)
noteable.id
else
noteable.iid
end
entity_url = build_entity_url(noteable_name.to_sym, noteable_id) noteable_id = noteable.respond_to?(:iid) ? noteable.iid : noteable.id
noteable_type = noteable_name(noteable)
entity_url = build_entity_url(noteable_type, noteable_id)
data = { data = {
user: { user: {
...@@ -157,11 +138,11 @@ class JiraService < IssueTrackerService ...@@ -157,11 +138,11 @@ class JiraService < IssueTrackerService
url: resource_url(user_path(author)), url: resource_url(user_path(author)),
}, },
project: { project: {
name: project.path_with_namespace, name: self.project.path_with_namespace,
url: resource_url(namespace_project_path(project.namespace, project)) url: resource_url(namespace_project_path(project.namespace, self.project))
}, },
entity: { entity: {
name: noteable_name.humanize.downcase, name: noteable_type.humanize.downcase,
url: entity_url, url: entity_url,
title: noteable.title title: noteable.title
} }
...@@ -193,8 +174,16 @@ class JiraService < IssueTrackerService ...@@ -193,8 +174,16 @@ class JiraService < IssueTrackerService
private private
def can_cross_reference?(noteable)
case noteable
when Commit then commit_events
when MergeRequest then merge_requests_events
else true
end
end
def close_issue(entity, issue) def close_issue(entity, issue)
return if issue.nil? || issue.resolution.present? return if issue.nil? || issue.resolution.present? || !jira_issue_transition_id.present?
commit_id = if entity.is_a?(Commit) commit_id = if entity.is_a?(Commit)
entity.id entity.id
...@@ -290,18 +279,26 @@ class JiraService < IssueTrackerService ...@@ -290,18 +279,26 @@ class JiraService < IssueTrackerService
"#{Settings.gitlab.base_url.chomp("/")}#{resource}" "#{Settings.gitlab.base_url.chomp("/")}#{resource}"
end end
def build_entity_url(entity_name, entity_id) def build_entity_url(noteable_type, entity_id)
polymorphic_url( polymorphic_url(
[ [
self.project.namespace.becomes(Namespace), self.project.namespace.becomes(Namespace),
self.project, self.project,
entity_name noteable_type.to_sym
], ],
id: entity_id, id: entity_id,
host: Settings.gitlab.base_url host: Settings.gitlab.base_url
) )
end end
def noteable_name(noteable)
name = noteable.model_name.singular
# ProjectSnippet inherits from Snippet class so it causes
# routing error building the URL.
name == "project_snippet" ? "snippet" : name
end
# Handle errors when doing JIRA API calls # Handle errors when doing JIRA API calls
def jira_request def jira_request
yield yield
......
This diff is collapsed.
...@@ -8,6 +8,7 @@ class Service < ActiveRecord::Base ...@@ -8,6 +8,7 @@ class Service < ActiveRecord::Base
default_value_for :push_events, true default_value_for :push_events, true
default_value_for :issues_events, true default_value_for :issues_events, true
default_value_for :confidential_issues_events, true default_value_for :confidential_issues_events, true
default_value_for :commit_events, true
default_value_for :merge_requests_events, true default_value_for :merge_requests_events, true
default_value_for :tag_push_events, true default_value_for :tag_push_events, true
default_value_for :note_events, true default_value_for :note_events, true
......
...@@ -7,6 +7,7 @@ class Snippet < ActiveRecord::Base ...@@ -7,6 +7,7 @@ class Snippet < ActiveRecord::Base
include Sortable include Sortable
include Elastic::SnippetsSearch include Elastic::SnippetsSearch
include Awardable include Awardable
include Mentionable
cache_markdown_field :title, pipeline: :single_line cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :content cache_markdown_field :content
......
...@@ -18,7 +18,9 @@ class Tree ...@@ -18,7 +18,9 @@ class Tree
def readme def readme
return @readme if defined?(@readme) return @readme if defined?(@readme)
available_readmes = blobs.select(&:readme?) available_readmes = blobs.select do |blob|
Gitlab::FileDetector.type_of(blob.name) == :readme
end
previewable_readmes = available_readmes.select do |blob| previewable_readmes = available_readmes.select do |blob|
previewable?(blob.name) previewable?(blob.name)
......
...@@ -247,19 +247,19 @@ class User < ActiveRecord::Base ...@@ -247,19 +247,19 @@ class User < ActiveRecord::Base
def filter(filter_name) def filter(filter_name)
case filter_name case filter_name
when 'admins' when 'admins'
self.admins admins
when 'blocked' when 'blocked'
self.blocked blocked
when 'two_factor_disabled' when 'two_factor_disabled'
self.without_two_factor without_two_factor
when 'two_factor_enabled' when 'two_factor_enabled'
self.with_two_factor with_two_factor
when 'wop' when 'wop'
self.without_projects without_projects
when 'external' when 'external'
self.external external
else else
self.active active
end end
end end
...@@ -378,7 +378,7 @@ class User < ActiveRecord::Base ...@@ -378,7 +378,7 @@ class User < ActiveRecord::Base
end end
def generate_password def generate_password
if self.force_random_password if force_random_password
self.password = self.password_confirmation = Devise.friendly_token.first(Devise.password_length.min) self.password = self.password_confirmation = Devise.friendly_token.first(Devise.password_length.min)
end end
end end
...@@ -419,56 +419,55 @@ class User < ActiveRecord::Base ...@@ -419,56 +419,55 @@ class User < ActiveRecord::Base
end end
def two_factor_otp_enabled? def two_factor_otp_enabled?
self.otp_required_for_login? otp_required_for_login?
end end
def two_factor_u2f_enabled? def two_factor_u2f_enabled?
self.u2f_registrations.exists? u2f_registrations.exists?
end end
def namespace_uniq def namespace_uniq
# Return early if username already failed the first uniqueness validation # Return early if username already failed the first uniqueness validation
return if self.errors.key?(:username) && return if errors.key?(:username) &&
self.errors[:username].include?('has already been taken') errors[:username].include?('has already been taken')
namespace_name = self.username existing_namespace = Namespace.by_path(username)
existing_namespace = Namespace.by_path(namespace_name) if existing_namespace && existing_namespace != namespace
if existing_namespace && existing_namespace != self.namespace errors.add(:username, 'has already been taken')
self.errors.add(:username, 'has already been taken')
end end
end end
def avatar_type def avatar_type
unless self.avatar.image? unless avatar.image?
self.errors.add :avatar, "only images allowed" errors.add :avatar, "only images allowed"
end end
end end
def unique_email def unique_email
if !self.emails.exists?(email: self.email) && Email.exists?(email: self.email) if !emails.exists?(email: email) && Email.exists?(email: email)
self.errors.add(:email, 'has already been taken') errors.add(:email, 'has already been taken')
end end
end end
def owns_notification_email def owns_notification_email
return if self.temp_oauth_email? return if temp_oauth_email?
self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email) errors.add(:notification_email, "is not an email you own") unless all_emails.include?(notification_email)
end end
def owns_public_email def owns_public_email
return if self.public_email.blank? return if public_email.blank?
self.errors.add(:public_email, "is not an email you own") unless self.all_emails.include?(self.public_email) errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
end end
def update_emails_with_primary_email def update_emails_with_primary_email
primary_email_record = self.emails.find_by(email: self.email) primary_email_record = emails.find_by(email: email)
if primary_email_record if primary_email_record
primary_email_record.destroy primary_email_record.destroy
self.emails.create(email: self.email_was) emails.create(email: email_was)
self.update_secondary_emails! update_secondary_emails!
end end
end end
...@@ -656,7 +655,7 @@ class User < ActiveRecord::Base ...@@ -656,7 +655,7 @@ class User < ActiveRecord::Base
end end
def project_deploy_keys def project_deploy_keys
DeployKey.unscoped.in_projects(self.authorized_projects.pluck(:id)).distinct(:id) DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id)
end end
def accessible_deploy_keys def accessible_deploy_keys
...@@ -672,38 +671,38 @@ class User < ActiveRecord::Base ...@@ -672,38 +671,38 @@ class User < ActiveRecord::Base
end end
def sanitize_attrs def sanitize_attrs
%w(name username skype linkedin twitter).each do |attr| %w[name username skype linkedin twitter].each do |attr|
value = self.send(attr) value = public_send(attr)
self.send("#{attr}=", Sanitize.clean(value)) if value.present? public_send("#{attr}=", Sanitize.clean(value)) if value.present?
end end
end end
def set_notification_email def set_notification_email
if self.notification_email.blank? || !self.all_emails.include?(self.notification_email) if notification_email.blank? || !all_emails.include?(notification_email)
self.notification_email = self.email self.notification_email = email
end end
end end
def set_public_email def set_public_email
if self.public_email.blank? || !self.all_emails.include?(self.public_email) if public_email.blank? || !all_emails.include?(public_email)
self.public_email = '' self.public_email = ''
end end
end end
def update_secondary_emails! def update_secondary_emails!
self.set_notification_email set_notification_email
self.set_public_email set_public_email
self.save if self.notification_email_changed? || self.public_email_changed? save if notification_email_changed? || public_email_changed?
end end
def set_projects_limit def set_projects_limit
# `User.select(:id)` raises # `User.select(:id)` raises
# `ActiveModel::MissingAttributeError: missing attribute: projects_limit` # `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
# without this safeguard! # without this safeguard!
return unless self.has_attribute?(:projects_limit) return unless has_attribute?(:projects_limit)
connection_default_value_defined = new_record? && !projects_limit_changed? connection_default_value_defined = new_record? && !projects_limit_changed?
return unless self.projects_limit.nil? || connection_default_value_defined return unless projects_limit.nil? || connection_default_value_defined
self.projects_limit = current_application_settings.default_projects_limit self.projects_limit = current_application_settings.default_projects_limit
end end
...@@ -733,7 +732,7 @@ class User < ActiveRecord::Base ...@@ -733,7 +732,7 @@ class User < ActiveRecord::Base
def with_defaults def with_defaults
User.defaults.each do |k, v| User.defaults.each do |k, v|
self.send("#{k}=", v) public_send("#{k}=", v)
end end
self self
...@@ -753,7 +752,7 @@ class User < ActiveRecord::Base ...@@ -753,7 +752,7 @@ class User < ActiveRecord::Base
# Thus it will automatically generate a new fragment # Thus it will automatically generate a new fragment
# when the event is updated because the key changes. # when the event is updated because the key changes.
def reset_events_cache def reset_events_cache
Event.where(author_id: self.id). Event.where(author_id: id).
order('id DESC').limit(1000). order('id DESC').limit(1000).
update_all(updated_at: Time.now) update_all(updated_at: Time.now)
end end
...@@ -786,8 +785,8 @@ class User < ActiveRecord::Base ...@@ -786,8 +785,8 @@ class User < ActiveRecord::Base
def all_emails def all_emails
all_emails = [] all_emails = []
all_emails << self.email unless self.temp_oauth_email? all_emails << email unless temp_oauth_email?
all_emails.concat(self.emails.map(&:email)) all_emails.concat(emails.map(&:email))
all_emails all_emails
end end
...@@ -801,21 +800,21 @@ class User < ActiveRecord::Base ...@@ -801,21 +800,21 @@ class User < ActiveRecord::Base
def ensure_namespace_correct def ensure_namespace_correct
# Ensure user has namespace # Ensure user has namespace
self.create_namespace!(path: self.username, name: self.username) unless self.namespace create_namespace!(path: username, name: username) unless namespace
if self.username_changed? if username_changed?
self.namespace.update_attributes(path: self.username, name: self.username) namespace.update_attributes(path: username, name: username)
end end
end end
def post_create_hook def post_create_hook
log_info("User \"#{self.name}\" (#{self.email}) was created") log_info("User \"#{name}\" (#{email}) was created")
notification_service.new_user(self, @reset_token) if self.created_by_id notification_service.new_user(self, @reset_token) if created_by_id
system_hook_service.execute_hooks_for(self, :create) system_hook_service.execute_hooks_for(self, :create)
end end
def post_destroy_hook def post_destroy_hook
log_info("User \"#{self.name}\" (#{self.email}) was removed") log_info("User \"#{name}\" (#{email}) was removed")
system_hook_service.execute_hooks_for(self, :destroy) system_hook_service.execute_hooks_for(self, :destroy)
end end
...@@ -863,7 +862,7 @@ class User < ActiveRecord::Base ...@@ -863,7 +862,7 @@ class User < ActiveRecord::Base
end end
def oauth_authorized_tokens def oauth_authorized_tokens
Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil) Doorkeeper::AccessToken.where(resource_owner_id: id, revoked_at: nil)
end end
# Returns the projects a user contributed to in the last year. # Returns the projects a user contributed to in the last year.
...@@ -994,7 +993,7 @@ class User < ActiveRecord::Base ...@@ -994,7 +993,7 @@ class User < ActiveRecord::Base
end end
def ensure_external_user_rights def ensure_external_user_rights
return unless self.external? return unless external?
self.can_create_group = false self.can_create_group = false
self.projects_limit = 0 self.projects_limit = 0
...@@ -1006,7 +1005,7 @@ class User < ActiveRecord::Base ...@@ -1006,7 +1005,7 @@ class User < ActiveRecord::Base
if current_application_settings.domain_blacklist_enabled? if current_application_settings.domain_blacklist_enabled?
blocked_domains = current_application_settings.domain_blacklist blocked_domains = current_application_settings.domain_blacklist
if domain_matches?(blocked_domains, self.email) if domain_matches?(blocked_domains, email)
error = 'is not from an allowed domain.' error = 'is not from an allowed domain.'
valid = false valid = false
end end
...@@ -1014,7 +1013,7 @@ class User < ActiveRecord::Base ...@@ -1014,7 +1013,7 @@ class User < ActiveRecord::Base
allowed_domains = current_application_settings.domain_whitelist allowed_domains = current_application_settings.domain_whitelist
unless allowed_domains.blank? unless allowed_domains.blank?
if domain_matches?(allowed_domains, self.email) if domain_matches?(allowed_domains, email)
valid = true valid = true
else else
error = "domain is not authorized for sign-up" error = "domain is not authorized for sign-up"
...@@ -1022,7 +1021,7 @@ class User < ActiveRecord::Base ...@@ -1022,7 +1021,7 @@ class User < ActiveRecord::Base
end end
end end
self.errors.add(:email, error) unless valid errors.add(:email, error) unless valid
valid valid
end end
......
...@@ -13,8 +13,11 @@ class IssuableEntity < Grape::Entity ...@@ -13,8 +13,11 @@ class IssuableEntity < Grape::Entity
expose :created_at expose :created_at
expose :updated_at expose :updated_at
expose :deleted_at expose :deleted_at
<<<<<<< HEAD
expose :time_estimate expose :time_estimate
expose :total_time_spent expose :total_time_spent
expose :human_time_estimate expose :human_time_estimate
expose :human_total_time_spent expose :human_total_time_spent
=======
>>>>>>> ce/master
end end
...@@ -4,7 +4,10 @@ class IssueEntity < IssuableEntity ...@@ -4,7 +4,10 @@ class IssueEntity < IssuableEntity
expose :due_date expose :due_date
expose :moved_to_id expose :moved_to_id
expose :project_id expose :project_id
<<<<<<< HEAD
expose :weight expose :weight
=======
>>>>>>> ce/master
expose :milestone, using: API::Entities::Milestone expose :milestone, using: API::Entities::Milestone
expose :labels, using: LabelEntity expose :labels, using: LabelEntity
end end
class MergeRequestEntity < IssuableEntity class MergeRequestEntity < IssuableEntity
<<<<<<< HEAD
expose :approvals_before_merge expose :approvals_before_merge
=======
>>>>>>> ce/master
expose :in_progress_merge_commit_sha expose :in_progress_merge_commit_sha
expose :locked_at expose :locked_at
expose :merge_commit_sha expose :merge_commit_sha
...@@ -8,8 +11,11 @@ class MergeRequestEntity < IssuableEntity ...@@ -8,8 +11,11 @@ class MergeRequestEntity < IssuableEntity
expose :merge_status expose :merge_status
expose :merge_user_id expose :merge_user_id
expose :merge_when_build_succeeds expose :merge_when_build_succeeds
<<<<<<< HEAD
expose :rebase_commit_sha expose :rebase_commit_sha
expose :rebase_in_progress?, if: { type: :full } expose :rebase_in_progress?, if: { type: :full }
=======
>>>>>>> ce/master
expose :source_branch expose :source_branch
expose :source_project_id expose :source_project_id
expose :target_branch expose :target_branch
......
...@@ -18,7 +18,7 @@ class GitPushService < BaseService ...@@ -18,7 +18,7 @@ class GitPushService < BaseService
# #
def execute def execute
@project.repository.after_create if @project.empty_repo? @project.repository.after_create if @project.empty_repo?
@project.repository.after_push_commit(branch_name, params[:newrev]) @project.repository.after_push_commit(branch_name)
if push_remove_branch? if push_remove_branch?
@project.repository.after_remove_branch @project.repository.after_remove_branch
...@@ -55,12 +55,32 @@ class GitPushService < BaseService ...@@ -55,12 +55,32 @@ class GitPushService < BaseService
execute_related_hooks execute_related_hooks
perform_housekeeping perform_housekeeping
update_caches
end end
def update_gitattributes def update_gitattributes
@project.repository.copy_gitattributes(params[:ref]) @project.repository.copy_gitattributes(params[:ref])
end end
def update_caches
if is_default_branch?
paths = Set.new
@push_commits.each do |commit|
commit.raw_diffs(deltas_only: true).each do |diff|
paths << diff.new_path
end
end
types = Gitlab::FileDetector.types_in_paths(paths.to_a)
else
types = []
end
ProjectCacheWorker.perform_async(@project.id, types)
end
protected protected
def execute_related_hooks def execute_related_hooks
...@@ -74,8 +94,12 @@ class GitPushService < BaseService ...@@ -74,8 +94,12 @@ class GitPushService < BaseService
EventCreateService.new.push(@project, current_user, build_push_data) EventCreateService.new.push(@project, current_user, build_push_data)
@project.execute_hooks(build_push_data.dup, :push_hooks) @project.execute_hooks(build_push_data.dup, :push_hooks)
@project.execute_services(build_push_data.dup, :push_hooks) @project.execute_services(build_push_data.dup, :push_hooks)
<<<<<<< HEAD
Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute(mirror_update: mirror_update) Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute(mirror_update: mirror_update)
ProjectCacheWorker.perform_async(@project.id) ProjectCacheWorker.perform_async(@project.id)
=======
Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute
>>>>>>> ce/master
if push_remove_branch? if push_remove_branch?
AfterBranchDeleteService AfterBranchDeleteService
......
module MergeRequests module MergeRequests
class AddTodoWhenBuildFailsService < MergeRequests::BaseService class AddTodoWhenBuildFailsService < MergeRequests::BaseService
# Adds a todo to the parent merge_request when a CI build fails # Adds a todo to the parent merge_request when a CI build fails
#
def execute(commit_status) def execute(commit_status)
return if commit_status.allow_failure?
commit_status_merge_requests(commit_status) do |merge_request| commit_status_merge_requests(commit_status) do |merge_request|
todo_service.merge_request_build_failed(merge_request) todo_service.merge_request_build_failed(merge_request)
end end
end end
# Closes any pending build failed todos for the parent MRs when a build is retried # Closes any pending build failed todos for the parent MRs when a
# build is retried
#
def close(commit_status) def close(commit_status)
commit_status_merge_requests(commit_status) do |merge_request| commit_status_merge_requests(commit_status) do |merge_request|
todo_service.merge_request_build_retried(merge_request) todo_service.merge_request_build_retried(merge_request)
......
...@@ -48,11 +48,11 @@ module MergeRequests ...@@ -48,11 +48,11 @@ module MergeRequests
end end
# See if source and target branches exist # See if source and target branches exist
unless merge_request.source_project.commit(merge_request.source_branch) if merge_request.source_branch.present? && !merge_request.source_project.commit(merge_request.source_branch)
messages << "Source branch \"#{merge_request.source_branch}\" does not exist" messages << "Source branch \"#{merge_request.source_branch}\" does not exist"
end end
unless merge_request.target_project.commit(merge_request.target_branch) if merge_request.target_branch.present? && !merge_request.target_project.commit(merge_request.target_branch)
messages << "Target branch \"#{merge_request.target_branch}\" does not exist" messages << "Target branch \"#{merge_request.target_branch}\" does not exist"
end end
......
...@@ -61,7 +61,15 @@ module MergeRequests ...@@ -61,7 +61,15 @@ module MergeRequests
merge_requests = filter_merge_requests(merge_requests) merge_requests = filter_merge_requests(merge_requests)
merge_requests.each do |merge_request| merge_requests.each do |merge_request|
reload_diff(merge_request) unless branch_removed? if merge_request.source_branch == @branch_name || force_push?
merge_request.reload_diff
else
mr_commit_ids = merge_request.commits.map(&:id)
push_commit_ids = @commits.map(&:id)
matches = mr_commit_ids & push_commit_ids
merge_request.reload_diff if matches.any?
end
merge_request.mark_as_unchecked merge_request.mark_as_unchecked
end end
end end
...@@ -180,16 +188,5 @@ module MergeRequests ...@@ -180,16 +188,5 @@ module MergeRequests
def branch_removed? def branch_removed?
Gitlab::Git.blank_ref?(@newrev) Gitlab::Git.blank_ref?(@newrev)
end end
def reload_diff(merge_request)
if merge_request.source_branch == @branch_name || force_push?
merge_request.reload_diff
else
mr_commit_ids = merge_request.commits.map(&:id)
push_commit_ids = @commits.map(&:id)
matches = mr_commit_ids & push_commit_ids
merge_request.reload_diff if matches.any?
end
end
end end
end end
...@@ -5,8 +5,6 @@ ...@@ -5,8 +5,6 @@
%div.form-group %div.form-group
= f.label :password = f.label :password
= f.password_field :password, class: "form-control bottom", required: true, title: "This field is required." = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required."
%div.submit-container.move-submit-down
= f.submit "Sign in", class: "btn btn-save"
- if devise_mapping.rememberable? - if devise_mapping.rememberable?
.remember-me.checkbox .remember-me.checkbox
%label{for: "user_remember_me"} %label{for: "user_remember_me"}
...@@ -14,3 +12,5 @@ ...@@ -14,3 +12,5 @@
%span Remember me %span Remember me
.pull-right.forgot-password .pull-right.forgot-password
= link_to "Forgot your password?", new_password_path(resource_name) = link_to "Forgot your password?", new_password_path(resource_name)
%div.submit-container.move-submit-down
= f.submit "Sign in", class: "btn btn-save"
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
= spinner = spinner
:javascript :javascript
var activity = new Activities(); var activity = new gl.Activities();
$(document).on('page:restore', function (event) { $(document).on('page:restore', function (event) {
activity.reloadActivities() activity.reloadActivities()
}) })
...@@ -11,12 +11,17 @@ ...@@ -11,12 +11,17 @@
= render 'projects/merge_requests/widget/open/geo' = render 'projects/merge_requests/widget/open/geo'
- if @project.archived? - if @project.archived?
= render 'projects/merge_requests/widget/open/archived' = render 'projects/merge_requests/widget/open/archived'
<<<<<<< HEAD
- elsif @project.above_size_limit? - elsif @project.above_size_limit?
= render 'projects/merge_requests/widget/open/size_limit_reached' = render 'projects/merge_requests/widget/open/size_limit_reached'
- elsif @merge_request.commits.blank? - elsif @merge_request.commits.blank?
= render 'projects/merge_requests/widget/open/nothing' = render 'projects/merge_requests/widget/open/nothing'
=======
>>>>>>> ce/master
- elsif @merge_request.branch_missing? - elsif @merge_request.branch_missing?
= render 'projects/merge_requests/widget/open/missing_branch' = render 'projects/merge_requests/widget/open/missing_branch'
- elsif @merge_request.commits.blank?
= render 'projects/merge_requests/widget/open/nothing'
- elsif @merge_request.unchecked? - elsif @merge_request.unchecked?
= render 'projects/merge_requests/widget/open/check' = render 'projects/merge_requests/widget/open/check'
- elsif @merge_request.cannot_be_merged? && !resolved_conflicts - elsif @merge_request.cannot_be_merged? && !resolved_conflicts
......
# Worker for updating any project specific caches. # Worker for updating any project specific caches.
#
# This worker runs at most once every 15 minutes per project. This is to ensure
# that multiple instances of jobs for this worker don't hammer the underlying
# storage engine as much.
class ProjectCacheWorker class ProjectCacheWorker
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
...@@ -10,46 +6,34 @@ class ProjectCacheWorker ...@@ -10,46 +6,34 @@ class ProjectCacheWorker
LEASE_TIMEOUT = 15.minutes.to_i LEASE_TIMEOUT = 15.minutes.to_i
def self.lease_for(project_id) # project_id - The ID of the project for which to flush the cache.
Gitlab::ExclusiveLease. # refresh - An Array containing extra types of data to refresh such as
new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT) # `:readme` to flush the README and `:changelog` to flush the
end # CHANGELOG.
def perform(project_id, refresh = [])
project = Project.find_by(id: project_id)
# Overwrite Sidekiq's implementation so we only schedule when actually needed. return unless project && project.repository.exists?
def self.perform_async(project_id)
# If a lease for this project is still being held there's no point in
# scheduling a new job.
super unless lease_for(project_id).exists?
end
def perform(project_id) update_repository_size(project)
if try_obtain_lease_for(project_id) project.update_commit_count
Rails.logger.
info("Obtained ProjectCacheWorker lease for project #{project_id}")
else
Rails.logger.
info("Could not obtain ProjectCacheWorker lease for project #{project_id}")
return
end
update_caches(project_id) project.repository.refresh_method_caches(refresh.map(&:to_sym))
end end
def update_caches(project_id) def update_repository_size(project)
project = Project.find(project_id) return unless try_obtain_lease_for(project.id, :update_repository_size)
return unless project.repository.exists? Rails.logger.info("Updating repository size for project #{project.id}")
project.update_repository_size project.update_repository_size
project.update_commit_count
if project.repository.root_ref
project.repository.build_cache
end
end end
def try_obtain_lease_for(project_id) private
self.class.lease_for(project_id).try_obtain
def try_obtain_lease_for(project_id, section)
Gitlab::ExclusiveLease.
new("project_cache_worker:#{project_id}:#{section}", timeout: LEASE_TIMEOUT).
try_obtain
end end
end end
---
title: Fix activity page endless scroll on large viewports
merge_request: 7608
author:
---
title: Fix regression causing bad error message to appear on Merge Request form
merge_request: 7599
author: Alex Sanford
---
title: Add deployment command to ChatOps
merge_request: 7619
author:
---
title: Add api endpoint for creating a pipeline
merge_request: 7209
author: Ido Leibovich
---
title: Fix 500 error when group name ends with git
merge_request: 7630
author:
---
title: Fix 404 on some group pages when name contains dot
merge_request: 7614
author:
---
title: Send credentials (currently for registry only) with build data to GitLab Runner
merge_request: 7474
author:
---
title: Added permissions per stage to cycle analytics endpoint
merge_request:
author:
---
title: Do not create a new TODO when failed build is allowed to fail
merge_request: 7618
author:
---
title: Do not create a MergeRequestDiff record when source branch is deleted
merge_request: 7481
author:
---
title: Fix errors happening when source branch of merge request is removed and then restored
merge_request: 7568
author:
---
title: Fix JIRA references for project snippets
merge_request:
author:
---
title: Allow enabling and disabling commit and MR events for JIRA
merge_request:
author:
---
title: Remove unnecessary self from user model
merge_request: 7551
author: Semyon Pupkov
---
title: Rework cache invalidation so only changed data is refreshed
merge_request: 7360
author:
...@@ -14,7 +14,9 @@ end ...@@ -14,7 +14,9 @@ end
resources :groups, only: [:index, :new, :create] resources :groups, only: [:index, :new, :create]
scope(path: 'groups/:id', controller: :groups) do scope(path: 'groups/:id',
controller: :groups,
constraints: { id: Gitlab::Regex.namespace_route_regex }) do
get :edit, as: :edit_group get :edit, as: :edit_group
get :issues, as: :issues_group get :issues, as: :issues_group
get :merge_requests, as: :merge_requests_group get :merge_requests, as: :merge_requests_group
...@@ -22,6 +24,7 @@ scope(path: 'groups/:id', controller: :groups) do ...@@ -22,6 +24,7 @@ scope(path: 'groups/:id', controller: :groups) do
get :activity, as: :activity_group get :activity, as: :activity_group
end end
<<<<<<< HEAD
scope(path: 'groups/:group_id', module: :groups, as: :group) do scope(path: 'groups/:group_id', module: :groups, as: :group) do
## EE-specific ## EE-specific
resource :analytics, only: [:show] resource :analytics, only: [:show]
...@@ -34,6 +37,12 @@ scope(path: 'groups/:group_id', module: :groups, as: :group) do ...@@ -34,6 +37,12 @@ scope(path: 'groups/:group_id', module: :groups, as: :group) do
resources :ldap_group_links, only: [:index, :create, :destroy] resources :ldap_group_links, only: [:index, :create, :destroy]
## EE-specific ## EE-specific
=======
scope(path: 'groups/:group_id',
module: :groups,
as: :group,
constraints: { group_id: Gitlab::Regex.namespace_route_regex }) do
>>>>>>> ce/master
resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
post :resend_invite, on: :member post :resend_invite, on: :member
delete :leave, on: :collection delete :leave, on: :collection
...@@ -61,4 +70,4 @@ scope(path: 'groups/:group_id', module: :groups, as: :group) do ...@@ -61,4 +70,4 @@ scope(path: 'groups/:group_id', module: :groups, as: :group) do
end end
# Must be last route in this file # Must be last route in this file
get 'groups/:id' => 'groups#show', as: :group_canonical get 'groups/:id' => 'groups#show', as: :group_canonical, constraints: { id: Gitlab::Regex.namespace_route_regex }
class AddCommitEventsToServices < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column_with_default(:services, :commit_events, :boolean, default: true, allow_null: false)
end
def down
remove_column(:services, :commit_events)
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20161117114805) do ActiveRecord::Schema.define(version: 20161118183841) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -115,9 +115,6 @@ ActiveRecord::Schema.define(version: 20161117114805) do ...@@ -115,9 +115,6 @@ ActiveRecord::Schema.define(version: 20161117114805) do
t.integer "housekeeping_incremental_repack_period", default: 10, null: false t.integer "housekeeping_incremental_repack_period", default: 10, null: false
t.integer "housekeeping_full_repack_period", default: 50, null: false t.integer "housekeeping_full_repack_period", default: 50, null: false
t.integer "housekeeping_gc_period", default: 200, null: false t.integer "housekeeping_gc_period", default: 200, null: false
t.boolean "sidekiq_throttling_enabled", default: false
t.string "sidekiq_throttling_queues"
t.decimal "sidekiq_throttling_factor"
end end
create_table "approvals", force: :cascade do |t| create_table "approvals", force: :cascade do |t|
...@@ -1214,6 +1211,7 @@ ActiveRecord::Schema.define(version: 20161117114805) do ...@@ -1214,6 +1211,7 @@ ActiveRecord::Schema.define(version: 20161117114805) do
t.boolean "wiki_page_events", default: true t.boolean "wiki_page_events", default: true
t.boolean "pipeline_events", default: false, null: false t.boolean "pipeline_events", default: false, null: false
t.boolean "confidential_issues_events", default: true, null: false t.boolean "confidential_issues_events", default: true, null: false
t.boolean "commit_events", default: true, null: false
end end
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
......
...@@ -114,6 +114,51 @@ Example of response ...@@ -114,6 +114,51 @@ Example of response
} }
``` ```
## Create a new pipeline
> [Introduced][ce-7209] in GitLab 8.14
```
POST /projects/:id/pipeline
```
| Attribute | Type | Required | Description |
|------------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a project |
| `ref` | string | yes | Reference to commit |
```
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipeline?ref=master"
```
Example of response
```json
{
"id": 61,
"sha": "384c444e840a515b23f21915ee5766b87068a70d",
"ref": "master",
"status": "pending",
"before_sha": "0000000000000000000000000000000000000000",
"tag": false,
"yaml_errors": null,
"user": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root"
},
"created_at": "2016-11-04T09:36:13.747Z",
"updated_at": "2016-11-04T09:36:13.977Z",
"started_at": null,
"finished_at": null,
"committed_at": null,
"duration": null
}
```
## Retry failed builds in a pipeline ## Retry failed builds in a pipeline
> [Introduced][ce-5837] in GitLab 8.11 > [Introduced][ce-5837] in GitLab 8.11
...@@ -205,3 +250,4 @@ Response: ...@@ -205,3 +250,4 @@ Response:
``` ```
[ce-5837]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5837 [ce-5837]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5837
[ce-7209]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7209
...@@ -235,7 +235,7 @@ will help us achieve that. ...@@ -235,7 +235,7 @@ will help us achieve that.
As the name suggests, it is possible to create environments on the fly by just As the name suggests, it is possible to create environments on the fly by just
declaring their names dynamically in `.gitlab-ci.yml`. Dynamic environments is declaring their names dynamically in `.gitlab-ci.yml`. Dynamic environments is
the basis of [Review apps](review_apps.md). the basis of [Review apps](review_apps/index.md).
GitLab Runner exposes various [environment variables][variables] when a job runs, GitLab Runner exposes various [environment variables][variables] when a job runs,
and as such, you can use them as environment names. Let's add another job in and as such, you can use them as environment names. Let's add another job in
......
...@@ -289,7 +289,7 @@ The trick is to use the merge/pull request with multiple commits when your work ...@@ -289,7 +289,7 @@ The trick is to use the merge/pull request with multiple commits when your work
The commit message should reflect your intention, not the contents of the commit. The commit message should reflect your intention, not the contents of the commit.
The contents of the commit can be easily seen anyway, the question is why you did it. The contents of the commit can be easily seen anyway, the question is why you did it.
An example of a good commit message is: "Combine templates to dry up the user views.". An example of a good commit message is: "Combine templates to dry up the user views.".
Some words that are bad commit messages because they don't contain munch information are: change, improve and refactor. Some words that are bad commit messages because they don't contain much information are: change, improve and refactor.
The word fix or fixes is also a red flag, unless it comes after the commit sentence and references an issue number. The word fix or fixes is also a red flag, unless it comes after the commit sentence and references an issue number.
To see more information about the formatting of commit messages please see this great [blog post by Tim Pope](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). To see more information about the formatting of commit messages please see this great [blog post by Tim Pope](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
......
...@@ -24,9 +24,11 @@ you still have open. ...@@ -24,9 +24,11 @@ you still have open.
A Todo appears in your Todos dashboard when: A Todo appears in your Todos dashboard when:
- an issue or merge request is assigned to you - an issue or merge request is assigned to you,
- you are `@mentioned` in an issue or merge request, be it the description of - you are `@mentioned` in an issue or merge request, be it the description of
the issue/merge request or in a comment the issue/merge request or in a comment,
- build in the CI pipeline running for your merge request failed, but this
build is not allowed to fail.
>**Note:** Commenting on a commit will _not_ trigger a Todo. >**Note:** Commenting on a commit will _not_ trigger a Todo.
......
...@@ -22,6 +22,27 @@ module API ...@@ -22,6 +22,27 @@ module API
pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope]) pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope])
present paginate(pipelines), with: Entities::Pipeline present paginate(pipelines), with: Entities::Pipeline
end end
desc 'Create a new pipeline' do
detail 'This feature was introduced in GitLab 8.14'
success Entities::Pipeline
end
params do
requires :ref, type: String, desc: 'Reference'
end
post ':id/pipeline' do
authorize! :create_pipeline, user_project
new_pipeline = Ci::CreatePipelineService.new(user_project,
current_user,
declared_params(include_missing: false))
.execute(ignore_skip_ci: true, save_on_errors: false)
if new_pipeline.persisted?
present new_pipeline, with: Entities::Pipeline
else
render_validation_error!(new_pipeline)
end
end
desc 'Gets a specific pipeline for the project' do desc 'Gets a specific pipeline for the project' do
detail 'This feature was introduced in GitLab 8.11' detail 'This feature was introduced in GitLab 8.11'
......
...@@ -3,6 +3,9 @@ module API ...@@ -3,6 +3,9 @@ module API
class ProjectSnippets < Grape::API class ProjectSnippets < Grape::API
before { authenticate! } before { authenticate! }
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects do resource :projects do
helpers do helpers do
def handle_project_member_errors(errors) def handle_project_member_errors(errors)
...@@ -18,111 +21,108 @@ module API ...@@ -18,111 +21,108 @@ module API
end end
end end
# Get a project snippets desc 'Get all project snippets' do
# success Entities::ProjectSnippet
# Parameters: end
# id (required) - The ID of a project
# Example Request:
# GET /projects/:id/snippets
get ":id/snippets" do get ":id/snippets" do
present paginate(snippets_for_current_user), with: Entities::ProjectSnippet present paginate(snippets_for_current_user), with: Entities::ProjectSnippet
end end
# Get a project snippet desc 'Get a single project snippet' do
# success Entities::ProjectSnippet
# Parameters: end
# id (required) - The ID of a project params do
# snippet_id (required) - The ID of a project snippet requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
# Example Request: end
# GET /projects/:id/snippets/:snippet_id
get ":id/snippets/:snippet_id" do get ":id/snippets/:snippet_id" do
@snippet = snippets_for_current_user.find(params[:snippet_id]) snippet = snippets_for_current_user.find(params[:snippet_id])
present @snippet, with: Entities::ProjectSnippet present snippet, with: Entities::ProjectSnippet
end end
# Create a new project snippet desc 'Create a new project snippet' do
# success Entities::ProjectSnippet
# Parameters: end
# id (required) - The ID of a project params do
# title (required) - The title of a snippet requires :title, type: String, desc: 'The title of the snippet'
# file_name (required) - The name of a snippet file requires :file_name, type: String, desc: 'The file name of the snippet'
# code (required) - The content of a snippet requires :code, type: String, desc: 'The content of the snippet'
# visibility_level (required) - The snippet's visibility requires :visibility_level, type: Integer,
# Example Request: values: [Gitlab::VisibilityLevel::PRIVATE,
# POST /projects/:id/snippets Gitlab::VisibilityLevel::INTERNAL,
Gitlab::VisibilityLevel::PUBLIC],
desc: 'The visibility level of the snippet'
end
post ":id/snippets" do post ":id/snippets" do
authorize! :create_project_snippet, user_project authorize! :create_project_snippet, user_project
required_attributes! [:title, :file_name, :code, :visibility_level] snippet_params = declared_params
snippet_params[:content] = snippet_params.delete(:code)
attrs = attributes_for_keys [:title, :file_name, :visibility_level] snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute
attrs[:content] = params[:code] if params[:code].present?
@snippet = CreateSnippetService.new(user_project, current_user,
attrs).execute
if @snippet.errors.any? if snippet.persisted?
render_validation_error!(@snippet) present snippet, with: Entities::ProjectSnippet
else else
present @snippet, with: Entities::ProjectSnippet render_validation_error!(snippet)
end end
end end
# Update an existing project snippet desc 'Update an existing project snippet' do
# success Entities::ProjectSnippet
# Parameters: end
# id (required) - The ID of a project params do
# snippet_id (required) - The ID of a project snippet requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
# title (optional) - The title of a snippet optional :title, type: String, desc: 'The title of the snippet'
# file_name (optional) - The name of a snippet file optional :file_name, type: String, desc: 'The file name of the snippet'
# code (optional) - The content of a snippet optional :code, type: String, desc: 'The content of the snippet'
# visibility_level (optional) - The snippet's visibility optional :visibility_level, type: Integer,
# Example Request: values: [Gitlab::VisibilityLevel::PRIVATE,
# PUT /projects/:id/snippets/:snippet_id Gitlab::VisibilityLevel::INTERNAL,
Gitlab::VisibilityLevel::PUBLIC],
desc: 'The visibility level of the snippet'
at_least_one_of :title, :file_name, :code, :visibility_level
end
put ":id/snippets/:snippet_id" do put ":id/snippets/:snippet_id" do
@snippet = snippets_for_current_user.find(params[:snippet_id]) snippet = snippets_for_current_user.find_by(id: params.delete(:snippet_id))
authorize! :update_project_snippet, @snippet not_found!('Snippet') unless snippet
authorize! :update_project_snippet, snippet
snippet_params = declared_params(include_missing: false)
snippet_params[:content] = snippet_params.delete(:code) if snippet_params[:code].present?
attrs = attributes_for_keys [:title, :file_name, :visibility_level] UpdateSnippetService.new(user_project, current_user, snippet,
attrs[:content] = params[:code] if params[:code].present? snippet_params).execute
UpdateSnippetService.new(user_project, current_user, @snippet, if snippet.persisted?
attrs).execute present snippet, with: Entities::ProjectSnippet
if @snippet.errors.any?
render_validation_error!(@snippet)
else else
present @snippet, with: Entities::ProjectSnippet render_validation_error!(snippet)
end end
end end
# Delete a project snippet desc 'Delete a project snippet'
# params do
# Parameters: requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
# id (required) - The ID of a project end
# snippet_id (required) - The ID of a project snippet
# Example Request:
# DELETE /projects/:id/snippets/:snippet_id
delete ":id/snippets/:snippet_id" do delete ":id/snippets/:snippet_id" do
begin snippet = snippets_for_current_user.find_by(id: params[:snippet_id])
@snippet = snippets_for_current_user.find(params[:snippet_id]) not_found!('Snippet') unless snippet
authorize! :update_project_snippet, @snippet
@snippet.destroy authorize! :admin_project_snippet, snippet
rescue snippet.destroy
not_found!('Snippet')
end
end end
# Get a raw project snippet desc 'Get a raw project snippet'
# params do
# Parameters: requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
# id (required) - The ID of a project end
# snippet_id (required) - The ID of a project snippet
# Example Request:
# GET /projects/:id/snippets/:snippet_id/raw
get ":id/snippets/:snippet_id/raw" do get ":id/snippets/:snippet_id/raw" do
@snippet = snippets_for_current_user.find(params[:snippet_id]) snippet = snippets_for_current_user.find_by(id: params[:snippet_id])
not_found!('Snippet') unless snippet
env['api.format'] = :txt env['api.format'] = :txt
content_type 'text/plain' content_type 'text/plain'
present @snippet.content present snippet.content
end end
end end
end end
......
...@@ -33,7 +33,10 @@ module API ...@@ -33,7 +33,10 @@ module API
optional :active, type: Boolean, default: false, desc: 'Filters only active users' optional :active, type: Boolean, default: false, desc: 'Filters only active users'
optional :external, type: Boolean, default: false, desc: 'Filters only external users' optional :external, type: Boolean, default: false, desc: 'Filters only external users'
optional :blocked, type: Boolean, default: false, desc: 'Filters only blocked users' optional :blocked, type: Boolean, default: false, desc: 'Filters only blocked users'
<<<<<<< HEAD
optional :skip_ldap, type: Boolean, default: false, desc: 'Skip LDAP users' optional :skip_ldap, type: Boolean, default: false, desc: 'Skip LDAP users'
=======
>>>>>>> ce/master
end end
get do get do
unless can?(current_user, :read_users_list, nil) unless can?(current_user, :read_users_list, nil)
...@@ -45,7 +48,10 @@ module API ...@@ -45,7 +48,10 @@ module API
else else
users = User.all users = User.all
users = users.active if params[:active] users = users.active if params[:active]
<<<<<<< HEAD
users = users.non_ldap if params[:skip_ldap] users = users.non_ldap if params[:skip_ldap]
=======
>>>>>>> ce/master
users = users.search(params[:search]) if params[:search].present? users = users.search(params[:search]) if params[:search].present?
users = users.blocked if params[:blocked] users = users.blocked if params[:blocked]
users = users.external if params[:external] && current_user.is_admin? users = users.external if params[:external] && current_user.is_admin?
......
...@@ -32,6 +32,10 @@ module Ci ...@@ -32,6 +32,10 @@ module Ci
expose :artifacts_file, using: ArtifactFile, if: ->(build, _) { build.artifacts? } expose :artifacts_file, using: ArtifactFile, if: ->(build, _) { build.artifacts? }
end end
class BuildCredentials < Grape::Entity
expose :type, :url, :username, :password
end
class BuildDetails < Build class BuildDetails < Build
expose :commands expose :commands
expose :repo_url expose :repo_url
...@@ -50,6 +54,8 @@ module Ci ...@@ -50,6 +54,8 @@ module Ci
expose :variables expose :variables
expose :depends_on_builds, using: Build expose :depends_on_builds, using: Build
expose :credentials, using: BuildCredentials
end end
class Runner < Grape::Entity class Runner < Grape::Entity
......
...@@ -4,6 +4,7 @@ module Gitlab ...@@ -4,6 +4,7 @@ module Gitlab
COMMANDS = [ COMMANDS = [
Gitlab::ChatCommands::IssueShow, Gitlab::ChatCommands::IssueShow,
Gitlab::ChatCommands::IssueCreate, Gitlab::ChatCommands::IssueCreate,
Gitlab::ChatCommands::Deploy,
].freeze ].freeze
def execute def execute
......
module Gitlab
module ChatCommands
class Deploy < BaseCommand
def self.match(text)
/\Adeploy\s+(?<from>.*)\s+to+\s+(?<to>.*)\z/.match(text)
end
def self.help_message
'deploy <environment> to <target-environment>'
end
def self.available?(project)
project.builds_enabled?
end
def self.allowed?(project, user)
can?(user, :create_deployment, project)
end
def execute(match)
from = match[:from]
to = match[:to]
actions = find_actions(from, to)
return unless actions.present?
if actions.one?
actions.first.play(current_user)
else
Result.new(:error, 'Too many actions defined')
end
end
private
def find_actions(from, to)
environment = project.environments.find_by(name: from)
return unless environment
environment.actions_for(to).select(&:starts_environment?)
end
end
end
end
module Gitlab
module ChatCommands
Result = Struct.new(:type, :message)
end
end
module Gitlab
module Ci
module Build
module Credentials
class Base
def type
self.class.name.demodulize.underscore
end
end
end
end
end
end
module Gitlab
module Ci
module Build
module Credentials
class Factory
def initialize(build)
@build = build
end
def create!
credentials.select(&:valid?)
end
private
def credentials
providers.map { |provider| provider.new(@build) }
end
def providers
[Registry]
end
end
end
end
end
end
module Gitlab
module Ci
module Build
module Credentials
class Registry < Base
attr_reader :username, :password
def initialize(build)
@username = 'gitlab-ci-token'
@password = build.token
end
def url
Gitlab.config.registry.host_port
end
def valid?
Gitlab.config.registry.enabled
end
end
end
end
end
end
module Gitlab
module CycleAnalytics
class Permissions
STAGE_PERMISSIONS = {
issue: :read_issue,
code: :read_merge_request,
test: :read_build,
review: :read_merge_request,
staging: :read_build,
production: :read_issue,
}.freeze
def self.get(*args)
new(*args).get
end
def initialize(user:, project:)
@user = user
@project = project
@stage_permission_hash = {}
end
def get
::CycleAnalytics::STAGES.each do |stage|
@stage_permission_hash[stage] = authorized_stage?(stage)
end
@stage_permission_hash
end
private
def authorized_stage?(stage)
return false unless authorize_project(:read_cycle_analytics)
STAGE_PERMISSIONS[stage] ? authorize_project(STAGE_PERMISSIONS[stage]) : true
end
def authorize_project(permission)
Ability.allowed?(@user, permission, @project)
end
end
end
end
require 'set'
module Gitlab
# Module that can be used to detect if a path points to a special file such as
# a README or a CONTRIBUTING file.
module FileDetector
PATTERNS = {
readme: /\Areadme/i,
changelog: /\A(changelog|history|changes|news)/i,
license: /\A(licen[sc]e|copying)(\..+|\z)/i,
contributing: /\Acontributing/i,
version: 'version',
gitignore: '.gitignore',
koding: '.koding.yml',
gitlab_ci: '.gitlab-ci.yml',
avatar: /\Alogo\.(png|jpg|gif)\z/
}
# Returns an Array of file types based on the given paths.
#
# This method can be used to check if a list of file paths (e.g. of changed
# files) involve any special files such as a README or a LICENSE file.
#
# Example:
#
# types_in_paths(%w{README.md foo/bar.txt}) # => [:readme]
def self.types_in_paths(paths)
types = Set.new
paths.each do |path|
type = type_of(path)
types << type if type
end
types.to_a
end
# Returns the type of a file path, or nil if none could be detected.
#
# Returned types are Symbols such as `:readme`, `:version`, etc.
#
# Example:
#
# type_of('README.md') # => :readme
# type_of('VERSION') # => :version
def self.type_of(path)
name = File.basename(path)
PATTERNS.each do |type, search|
did_match = if search.is_a?(Regexp)
name =~ search
else
name.casecmp(search) == 0
end
return type if did_match
end
nil
end
end
end
...@@ -9,7 +9,7 @@ module Gitlab ...@@ -9,7 +9,7 @@ module Gitlab
# `NAMESPACE_REGEX_STR`, with the negative lookbehind assertion removed. This means that the client-side validation # `NAMESPACE_REGEX_STR`, with the negative lookbehind assertion removed. This means that the client-side validation
# will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation. # will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation.
NAMESPACE_REGEX_STR_SIMPLE = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze NAMESPACE_REGEX_STR_SIMPLE = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze
NAMESPACE_REGEX_STR = "(?:#{NAMESPACE_REGEX_STR_SIMPLE})(?<!\.git|\.atom)".freeze NAMESPACE_REGEX_STR = '(?:' + NAMESPACE_REGEX_STR_SIMPLE + ')(?<!\.git|\.atom)'.freeze
def namespace_regex def namespace_regex
@namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze @namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze
......
...@@ -24,20 +24,22 @@ module Mattermost ...@@ -24,20 +24,22 @@ module Mattermost
end end
end end
def present(resource) def present(subject)
return not_found unless resource return not_found unless subject
if resource.respond_to?(:count) if subject.is_a?(Gitlab::ChatCommands::Result)
if resource.count > 1 show_result(subject)
return multiple_resources(resource) elsif subject.respond_to?(:count)
elsif resource.count == 0 if subject.many?
return not_found multiple_resources(subject)
elsif subject.none?
not_found
else else
resource = resource.first single_resource(subject)
end end
else
single_resource(subject)
end end
single_resource(resource)
end end
def access_denied def access_denied
...@@ -46,6 +48,10 @@ module Mattermost ...@@ -46,6 +48,10 @@ module Mattermost
private private
def show_result(result)
ephemeral_response(result.message)
end
def not_found def not_found
ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:") ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:")
end end
...@@ -54,7 +60,7 @@ module Mattermost ...@@ -54,7 +60,7 @@ module Mattermost
return error(resource) if resource.errors.any? || !resource.persisted? return error(resource) if resource.errors.any? || !resource.persisted?
message = "### #{title(resource)}" message = "### #{title(resource)}"
message << "\n\n#{resource.description}" if resource.description message << "\n\n#{resource.description}" if resource.try(:description)
in_channel_response(message) in_channel_response(message)
end end
...@@ -74,7 +80,10 @@ module Mattermost ...@@ -74,7 +80,10 @@ module Mattermost
end end
def title(resource) def title(resource)
"[#{resource.to_reference} #{resource.title}](#{url(resource)})" reference = resource.try(:to_reference) || resource.try(:id)
title = resource.try(:title) || resource.try(:name)
"[#{reference} #{title}](#{url(resource)})"
end end
def header_with_list(header, items) def header_with_list(header, items)
......
This diff is collapsed.
#!/bin/bash #!/bin/sh
retry() { retry() {
if eval "$@"; then if eval "$@"; then
...@@ -24,11 +24,12 @@ if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then ...@@ -24,11 +24,12 @@ if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then
cp config/resque.yml.example config/resque.yml cp config/resque.yml.example config/resque.yml
sed -i 's/localhost/redis/g' config/resque.yml sed -i 's/localhost/redis/g' config/resque.yml
export FLAGS=(--path vendor --retry 3 --quiet) export FLAGS="--path vendor --retry 3 --quiet"
else else
export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin rnd=$(awk 'BEGIN { srand() ; printf("%d\n",rand()*5) }')
export PATH="$HOME/bin:/usr/local/bin:/usr/bin:/bin"
cp config/database.yml.mysql config/database.yml cp config/database.yml.mysql config/database.yml
sed "s/username\:.*$/username\: runner/" -i config/database.yml sed "s/username\:.*$/username\: runner/" -i config/database.yml
sed "s/password\:.*$/password\: 'password'/" -i config/database.yml sed "s/password\:.*$/password\: 'password'/" -i config/database.yml
sed "s/gitlabhq_test/gitlabhq_test_$((RANDOM/5000))/" -i config/database.yml sed "s/gitlabhq_test/gitlabhq_test_$rnd/" -i config/database.yml
fi fi
require 'spec_helper'
describe Projects::CycleAnalyticsController do
let(:project) { create(:project) }
let(:user) { create(:user) }
before do
sign_in(user)
project.team << [user, :master]
end
describe 'cycle analytics not set up flag' do
context 'with no data' do
it 'is true' do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param)
expect(response).to be_success
expect(assigns(:cycle_analytics_no_data)).to eq(true)
end
end
context 'with data' do
before do
issue = create(:issue, project: project, created_at: 4.days.ago)
milestone = create(:milestone, project: project, created_at: 5.days.ago)
issue.update(milestone: milestone)
create_merge_request_closing_issue(issue)
end
it 'is false' do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param)
expect(response).to be_success
expect(assigns(:cycle_analytics_no_data)).to eq(false)
end
end
end
end
...@@ -55,6 +55,12 @@ FactoryGirl.define do ...@@ -55,6 +55,12 @@ FactoryGirl.define do
self.when 'manual' self.when 'manual'
end end
trait :teardown_environment do
options do
{ environment: { action: 'stop' } }
end
end
trait :allowed_to_fail do trait :allowed_to_fail do
allow_failure true allow_failure true
end end
......
...@@ -106,4 +106,11 @@ feature 'Create New Merge Request', feature: true, js: true do ...@@ -106,4 +106,11 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).to have_content "6049019_460s.jpg" expect(page).to have_content "6049019_460s.jpg"
end end
end end
# Isolates a regression (see #24627)
it 'does not show error messages on initial form' do
visit new_namespace_project_merge_request_path(project.namespace, project)
expect(page).not_to have_selector('#error_explanation')
expect(page).not_to have_content('The form contains the following error')
end
end end
require 'spec_helper'
describe 'Deleted source branch', feature: true, js: true do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
before do
login_as user
merge_request.project.team << [user, :master]
merge_request.update!(source_branch: 'this-branch-does-not-exist')
visit namespace_project_merge_request_path(
merge_request.project.namespace,
merge_request.project, merge_request
)
end
it 'shows a message about missing source branch' do
expect(page).to have_content(
'Source branch this-branch-does-not-exist does not exist'
)
end
it 'hides Discussion, Commits and Changes tabs' do
within '.merge-request-details' do
expect(page).to have_no_content('Discussion')
expect(page).to have_no_content('Commits')
expect(page).to have_no_content('Changes')
end
end
end
...@@ -3,11 +3,12 @@ require 'spec_helper' ...@@ -3,11 +3,12 @@ require 'spec_helper'
feature 'Merge Request versions', js: true, feature: true do feature 'Merge Request versions', js: true, feature: true do
let(:merge_request) { create(:merge_request, importing: true) } let(:merge_request) { create(:merge_request, importing: true) }
let(:project) { merge_request.source_project } let(:project) { merge_request.source_project }
let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
let!(:merge_request_diff2) { merge_request.merge_request_diffs.create(head_commit_sha: nil) }
let!(:merge_request_diff3) { merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
before do before do
login_as :admin login_as :admin
merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
end end
...@@ -53,7 +54,7 @@ feature 'Merge Request versions', js: true, feature: true do ...@@ -53,7 +54,7 @@ feature 'Merge Request versions', js: true, feature: true do
project.namespace, project.namespace,
project, project,
merge_request.iid, merge_request.iid,
diff_id: 2, diff_id: merge_request_diff3.id,
start_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' start_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
) )
end end
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
describe('Activities', () => { describe('Activities', () => {
beforeEach(() => { beforeEach(() => {
fixture.load(fixtureTemplate); fixture.load(fixtureTemplate);
new Activities(); new gl.Activities();
}); });
for(let i = 0; i < filters.length; i++) { for(let i = 0; i < filters.length; i++) {
......
...@@ -135,7 +135,7 @@ describe('Environment item', () => { ...@@ -135,7 +135,7 @@ describe('Environment item', () => {
}); });
it('should render environment name', () => { it('should render environment name', () => {
expect(component.$el.querySelector('.environment-name').textContent).toEqual(environment.name); expect(component.$el.querySelector('.environment-name').textContent).toContain(environment.name);
}); });
describe('With deployment', () => { describe('With deployment', () => {
......
/* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, indent, quote-props, no-var, padded-blocks, max-len */ /* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, indent, quote-props, no-var, padded-blocks, max-len */
/*= require merge_request_widget */ /*= require merge_request_widget */
/*= require lib/utils/timeago.js */ /*= require lib/utils/timeago */
/*= require lib/utils/datetime_utility */
(function() { (function() {
describe('MergeRequestWidget', function() { describe('MergeRequestWidget', function() {
...@@ -35,9 +36,9 @@ ...@@ -35,9 +36,9 @@
external_url_formatted: 'test-url.com' external_url_formatted: 'test-url.com'
}]; }];
spyOn(jQuery, 'getJSON').and.callFake((req, cb) => { spyOn(jQuery, 'getJSON').and.callFake(function(req, cb) {
cb(this.ciEnvironmentsStatusData); cb(this.ciEnvironmentsStatusData);
}); }.bind(this));
}); });
it('should call renderEnvironments when the environments property is set', function() { it('should call renderEnvironments when the environments property is set', function() {
...@@ -54,6 +55,57 @@ ...@@ -54,6 +55,57 @@
}); });
}); });
describe('renderEnvironments', function() {
describe('should render correct timeago', function() {
beforeEach(function() {
this.environments = [{
id: 'test-environment-id',
url: 'testurl',
deployed_at: new Date().toISOString(),
deployed_at_formatted: true
}];
});
function getTimeagoText(template) {
var el = document.createElement('html');
el.innerHTML = template;
return el.querySelector('.js-environment-timeago').innerText.trim();
}
it('should render less than a minute ago text', function() {
spyOn(this.class.$widgetBody, 'before').and.callFake(function(template) {
expect(getTimeagoText(template)).toBe('less than a minute ago.');
});
this.class.renderEnvironments(this.environments);
});
it('should render about an hour ago text', function() {
var oneHourAgo = new Date();
oneHourAgo.setHours(oneHourAgo.getHours() - 1);
this.environments[0].deployed_at = oneHourAgo.toISOString();
spyOn(this.class.$widgetBody, 'before').and.callFake(function(template) {
expect(getTimeagoText(template)).toBe('about an hour ago.');
});
this.class.renderEnvironments(this.environments);
});
it('should render about 2 hours ago text', function() {
var twoHoursAgo = new Date();
twoHoursAgo.setHours(twoHoursAgo.getHours() - 2);
this.environments[0].deployed_at = twoHoursAgo.toISOString();
spyOn(this.class.$widgetBody, 'before').and.callFake(function(template) {
expect(getTimeagoText(template)).toBe('about 2 hours ago.');
});
this.class.renderEnvironments(this.environments);
});
});
});
return describe('getCIStatus', function() { return describe('getCIStatus', function() {
beforeEach(function() { beforeEach(function() {
this.ciStatusData = { this.ciStatusData = {
......
...@@ -4,9 +4,9 @@ describe Gitlab::ChatCommands::Command, service: true do ...@@ -4,9 +4,9 @@ describe Gitlab::ChatCommands::Command, service: true do
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:user) { create(:user) } let(:user) { create(:user) }
subject { described_class.new(project, user, params).execute }
describe '#execute' do describe '#execute' do
subject { described_class.new(project, user, params).execute }
context 'when no command is available' do context 'when no command is available' do
let(:params) { { text: 'issue show 1' } } let(:params) { { text: 'issue show 1' } }
let(:project) { create(:project, has_external_issue_tracker: true) } let(:project) { create(:project, has_external_issue_tracker: true) }
...@@ -51,5 +51,44 @@ describe Gitlab::ChatCommands::Command, service: true do ...@@ -51,5 +51,44 @@ describe Gitlab::ChatCommands::Command, service: true do
expect(subject[:text]).to match(/\/issues\/\d+/) expect(subject[:text]).to match(/\/issues\/\d+/)
end end
end end
context 'when trying to do deployment' do
let(:params) { { text: 'deploy staging to production' } }
let!(:build) { create(:ci_build, project: project) }
let!(:staging) { create(:environment, name: 'staging', project: project) }
let!(:deployment) { create(:deployment, environment: staging, deployable: build) }
let!(:manual) do
create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'first', environment: 'production')
end
context 'and user can not create deployment' do
it 'returns action' do
expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to start_with('Whoops! That action is not allowed')
end
end
context 'and user does have deployment permission' do
before do
project.team << [user, :developer]
end
it 'returns action' do
expect(subject[:text]).to include(manual.name)
expect(subject[:response_type]).to be(:in_channel)
end
context 'when duplicate action exists' do
let!(:manual2) do
create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'second', environment: 'production')
end
it 'returns error' do
expect(subject[:response_type]).to be(:ephemeral)
expect(subject[:text]).to include('Too many actions defined')
end
end
end
end
end end
end end
require 'spec_helper'
describe Gitlab::ChatCommands::Deploy, service: true do
describe '#execute' do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
let(:regex_match) { described_class.match('deploy staging to production') }
before do
project.team << [user, :master]
end
subject do
described_class.new(project, user).execute(regex_match)
end
context 'if no environment is defined' do
it 'returns nil' do
expect(subject).to be_nil
end
end
context 'with environment' do
let!(:staging) { create(:environment, name: 'staging', project: project) }
let!(:build) { create(:ci_build, project: project) }
let!(:deployment) { create(:deployment, environment: staging, deployable: build) }
context 'without actions' do
it 'returns nil' do
expect(subject).to be_nil
end
end
context 'with action' do
let!(:manual1) do
create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'first', environment: 'production')
end
it 'returns action' do
expect(subject).to eq(manual1)
end
context 'when duplicate action exists' do
let!(:manual2) do
create(:ci_build, :manual, project: project, pipeline: build.pipeline, name: 'second', environment: 'production')
end
it 'returns error' do
expect(subject.message).to eq('Too many actions defined')
end
end
context 'when teardown action exists' do
let!(:teardown) do
create(:ci_build, :manual, :teardown_environment,
project: project, pipeline: build.pipeline,
name: 'teardown', environment: 'production')
end
it 'returns error' do
expect(subject).to eq(manual1)
end
end
end
end
end
describe 'self.match' do
it 'matches the environment' do
match = described_class.match('deploy staging to production')
expect(match[:from]).to eq('staging')
expect(match[:to]).to eq('production')
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Build::Credentials::Factory do
let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) }
subject { Gitlab::Ci::Build::Credentials::Factory.new(build).create! }
class TestProvider
def initialize(build); end
end
before do
allow_any_instance_of(Gitlab::Ci::Build::Credentials::Factory).to receive(:providers).and_return([TestProvider])
end
context 'when provider is valid' do
before do
allow_any_instance_of(TestProvider).to receive(:valid?).and_return(true)
end
it 'generates an array of credentials objects' do
is_expected.to be_kind_of(Array)
is_expected.not_to be_empty
expect(subject.first).to be_kind_of(TestProvider)
end
end
context 'when provider is not valid' do
before do
allow_any_instance_of(TestProvider).to receive(:valid?).and_return(false)
end
it 'generates an array without specific credential object' do
is_expected.to be_kind_of(Array)
is_expected.to be_empty
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Build::Credentials::Registry do
let(:build) { create(:ci_build, name: 'spinach', stage: 'test', stage_idx: 0) }
let(:registry_url) { 'registry.example.com:5005' }
subject { Gitlab::Ci::Build::Credentials::Registry.new(build) }
before do
stub_container_registry_config(host_port: registry_url)
end
it 'contains valid DockerRegistry credentials' do
expect(subject).to be_kind_of(Gitlab::Ci::Build::Credentials::Registry)
expect(subject.username).to eq 'gitlab-ci-token'
expect(subject.password).to eq build.token
expect(subject.url).to eq registry_url
expect(subject.type).to eq 'registry'
end
describe '.valid?' do
subject { Gitlab::Ci::Build::Credentials::Registry.new(build).valid? }
context 'when registry is enabled' do
before do
stub_container_registry_config(enabled: true)
end
it { is_expected.to be_truthy }
end
context 'when registry is disabled' do
before do
stub_container_registry_config(enabled: false)
end
it { is_expected.to be_falsey }
end
end
end
require 'spec_helper'
describe Gitlab::CycleAnalytics::Permissions do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
subject { described_class.get(user: user, project: project) }
context 'user with no relation to the project' do
it 'has no permissions to issue stage' do
expect(subject[:issue]).to eq(false)
end
it 'has no permissions to test stage' do
expect(subject[:test]).to eq(false)
end
it 'has no permissions to staging stage' do
expect(subject[:staging]).to eq(false)
end
it 'has no permissions to production stage' do
expect(subject[:production]).to eq(false)
end
it 'has no permissions to code stage' do
expect(subject[:code]).to eq(false)
end
it 'has no permissions to review stage' do
expect(subject[:review]).to eq(false)
end
it 'has no permissions to plan stage' do
expect(subject[:plan]).to eq(false)
end
end
context 'user is master' do
before do
project.team << [user, :master]
end
it 'has permissions to issue stage' do
expect(subject[:issue]).to eq(true)
end
it 'has permissions to test stage' do
expect(subject[:test]).to eq(true)
end
it 'has permissions to staging stage' do
expect(subject[:staging]).to eq(true)
end
it 'has permissions to production stage' do
expect(subject[:production]).to eq(true)
end
it 'has permissions to code stage' do
expect(subject[:code]).to eq(true)
end
it 'has permissions to review stage' do
expect(subject[:review]).to eq(true)
end
it 'has permissions to plan stage' do
expect(subject[:plan]).to eq(true)
end
end
context 'user has no build permissions' do
before do
project.team << [user, :guest]
end
it 'has permissions to issue stage' do
expect(subject[:issue]).to eq(true)
end
it 'has no permissions to test stage' do
expect(subject[:test]).to eq(false)
end
it 'has no permissions to staging stage' do
expect(subject[:staging]).to eq(false)
end
end
context 'user has no merge request permissions' do
before do
project.team << [user, :guest]
end
it 'has permissions to issue stage' do
expect(subject[:issue]).to eq(true)
end
it 'has no permissions to code stage' do
expect(subject[:code]).to eq(false)
end
it 'has no permissions to review stage' do
expect(subject[:review]).to eq(false)
end
end
context 'user has no issue permissions' do
before do
project.team << [user, :developer]
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
end
it 'has permissions to code stage' do
expect(subject[:code]).to eq(true)
end
it 'has no permissions to issue stage' do
expect(subject[:issue]).to eq(false)
end
it 'has no permissions to production stage' do
expect(subject[:production]).to eq(false)
end
end
end
require 'spec_helper'
describe Gitlab::FileDetector do
describe '.types_in_paths' do
it 'returns the file types for the given paths' do
expect(described_class.types_in_paths(%w(README.md CHANGELOG VERSION VERSION))).
to eq(%i{readme changelog version})
end
it 'does not include unrecognized file paths' do
expect(described_class.types_in_paths(%w(README.md foo.txt))).
to eq(%i{readme})
end
end
describe '.type_of' do
it 'returns the type of a README file' do
expect(described_class.type_of('README.md')).to eq(:readme)
end
it 'returns the type of a changelog file' do
%w(CHANGELOG HISTORY CHANGES NEWS).each do |file|
expect(described_class.type_of(file)).to eq(:changelog)
end
end
it 'returns the type of a license file' do
%w(LICENSE LICENCE COPYING).each do |file|
expect(described_class.type_of(file)).to eq(:license)
end
end
it 'returns the type of a version file' do
expect(described_class.type_of('VERSION')).to eq(:version)
end
it 'returns the type of a .gitignore file' do
expect(described_class.type_of('.gitignore')).to eq(:gitignore)
end
it 'returns the type of a Koding config file' do
expect(described_class.type_of('.koding.yml')).to eq(:koding)
end
it 'returns the type of a GitLab CI config file' do
expect(described_class.type_of('.gitlab-ci.yml')).to eq(:gitlab_ci)
end
it 'returns the type of an avatar' do
%w(logo.gif logo.png logo.jpg).each do |file|
expect(described_class.type_of(file)).to eq(:avatar)
end
end
it 'returns nil for an unknown file' do
expect(described_class.type_of('foo.txt')).to be_nil
end
end
end
...@@ -262,6 +262,7 @@ Service: ...@@ -262,6 +262,7 @@ Service:
- template - template
- push_events - push_events
- issues_events - issues_events
- commit_events
- merge_requests_events - merge_requests_events
- tag_push_events - tag_push_events
- note_events - note_events
...@@ -348,6 +349,7 @@ LabelPriority: ...@@ -348,6 +349,7 @@ LabelPriority:
- priority - priority
- created_at - created_at
- updated_at - updated_at
<<<<<<< HEAD
Timelog: Timelog:
- id - id
- time_spent - time_spent
...@@ -356,3 +358,5 @@ Timelog: ...@@ -356,3 +358,5 @@ Timelog:
- user_id - user_id
- created_at - created_at
- updated_at - updated_at
=======
>>>>>>> ce/master
require 'spec_helper' require 'spec_helper'
describe BroadcastMessage, models: true do describe BroadcastMessage, models: true do
subject { create(:broadcast_message) } subject { build(:broadcast_message) }
it { is_expected.to be_valid } it { is_expected.to be_valid }
......
...@@ -9,6 +9,7 @@ describe Environment, models: true do ...@@ -9,6 +9,7 @@ describe Environment, models: true do
it { is_expected.to delegate_method(:last_deployment).to(:deployments).as(:last) } it { is_expected.to delegate_method(:last_deployment).to(:deployments).as(:last) }
it { is_expected.to delegate_method(:stop_action).to(:last_deployment) } it { is_expected.to delegate_method(:stop_action).to(:last_deployment) }
it { is_expected.to delegate_method(:manual_actions).to(:last_deployment) }
it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) } it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
...@@ -187,4 +188,15 @@ describe Environment, models: true do ...@@ -187,4 +188,15 @@ describe Environment, models: true do
it { is_expected.to be false } it { is_expected.to be false }
end end
end end
describe '#actions_for' do
let(:deployment) { create(:deployment, environment: environment) }
let(:pipeline) { deployment.deployable.pipeline }
let!(:review_action) { create(:ci_build, :manual, name: 'review-apps', pipeline: pipeline, environment: 'review/$CI_BUILD_REF_NAME' )}
let!(:production_action) { create(:ci_build, :manual, name: 'production', pipeline: pipeline, environment: 'production' )}
it 'returns a list of actions with matching environment' do
expect(environment.actions_for('review/master')).to contain_exactly(review_action)
end
end
end end
...@@ -83,7 +83,8 @@ describe JiraService, models: true do ...@@ -83,7 +83,8 @@ describe JiraService, models: true do
url: 'http://jira.example.com', url: 'http://jira.example.com',
username: 'gitlab_jira_username', username: 'gitlab_jira_username',
password: 'gitlab_jira_password', password: 'gitlab_jira_password',
project_key: 'GitLabProject' project_key: 'GitLabProject',
jira_issue_transition_id: "custom-id"
) )
# These stubs are needed to test JiraService#close_issue. # These stubs are needed to test JiraService#close_issue.
...@@ -177,11 +178,10 @@ describe JiraService, models: true do ...@@ -177,11 +178,10 @@ describe JiraService, models: true do
end end
it "calls the api with jira_issue_transition_id" do it "calls the api with jira_issue_transition_id" do
@jira_service.jira_issue_transition_id = 'this-is-a-custom-id'
@jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @transitions_url).with( expect(WebMock).to have_requested(:post, @transitions_url).with(
body: /this-is-a-custom-id/ body: /custom-id/
).once ).once
end end
......
...@@ -1859,7 +1859,7 @@ describe Project, models: true do ...@@ -1859,7 +1859,7 @@ describe Project, models: true do
end end
it 'expires the avatar cache' do it 'expires the avatar cache' do
expect(project.repository).to receive(:expire_avatar_cache).with(project.default_branch) expect(project.repository).to receive(:expire_avatar_cache)
project.change_head(project.default_branch) project.change_head(project.default_branch)
end end
......
This diff is collapsed.
...@@ -14,7 +14,7 @@ describe API::API, api: true do ...@@ -14,7 +14,7 @@ describe API::API, api: true do
describe "GET /projects/:id/repository/branches" do describe "GET /projects/:id/repository/branches" do
it "returns an array of project branches" do it "returns an array of project branches" do
project.repository.expire_cache project.repository.expire_all_method_caches
get api("/projects/#{project.id}/repository/branches", user) get api("/projects/#{project.id}/repository/branches", user)
expect(response).to have_http_status(200) expect(response).to have_http_status(200)
......
...@@ -41,6 +41,52 @@ describe API::API, api: true do ...@@ -41,6 +41,52 @@ describe API::API, api: true do
end end
end end
describe 'POST /projects/:id/pipeline ' do
context 'authorized user' do
context 'with gitlab-ci.yml' do
before { stub_ci_pipeline_to_return_yaml_file }
it 'creates and returns a new pipeline' do
expect do
post api("/projects/#{project.id}/pipeline", user), ref: project.default_branch
end.to change { Ci::Pipeline.count }.by(1)
expect(response).to have_http_status(201)
expect(json_response).to be_a Hash
expect(json_response['sha']).to eq project.commit.id
end
it 'fails when using an invalid ref' do
post api("/projects/#{project.id}/pipeline", user), ref: 'invalid_ref'
expect(response).to have_http_status(400)
expect(json_response['message']['base'].first).to eq 'Reference not found'
expect(json_response).not_to be_an Array
end
end
context 'without gitlab-ci.yml' do
it 'fails to create pipeline' do
post api("/projects/#{project.id}/pipeline", user), ref: project.default_branch
expect(response).to have_http_status(400)
expect(json_response['message']['base'].first).to eq 'Missing .gitlab-ci.yml file'
expect(json_response).not_to be_an Array
end
end
end
context 'unauthorized user' do
it 'does not create pipeline' do
post api("/projects/#{project.id}/pipeline", non_member), ref: project.default_branch
expect(response).to have_http_status(404)
expect(json_response['message']).to eq '404 Project Not Found'
expect(json_response).not_to be_an Array
end
end
end
describe 'GET /projects/:id/pipelines/:pipeline_id' do describe 'GET /projects/:id/pipelines/:pipeline_id' do
context 'authorized user' do context 'authorized user' do
it 'returns project pipelines' do it 'returns project pipelines' do
......
This diff is collapsed.
This diff is collapsed.
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