Commit 77daed05 authored by Regis's avatar Regis

merge master

parents d46af1d9 632450a4
......@@ -132,7 +132,7 @@ gem 'after_commit_queue', '~> 1.3.0'
gem 'acts-as-taggable-on', '~> 4.0'
# Background jobs
gem 'sidekiq', '~> 4.2'
gem 'sidekiq', '~> 4.2.7'
gem 'sidekiq-cron', '~> 0.4.4'
gem 'redis-namespace', '~> 1.5.2'
gem 'sidekiq-limit_fetch', '~> 3.4'
......
......@@ -126,7 +126,7 @@ GEM
coffee-script-source (1.10.0)
colorize (0.7.7)
concurrent-ruby (1.0.2)
connection_pool (2.2.0)
connection_pool (2.2.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
creole (0.5.0)
......@@ -648,10 +648,10 @@ GEM
rack
shoulda-matchers (2.8.0)
activesupport (>= 3.0.0)
sidekiq (4.2.1)
sidekiq (4.2.7)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
rack-protection (~> 1.5)
rack-protection (>= 1.5.0)
redis (~> 3.2, >= 3.2.1)
sidekiq-cron (0.4.4)
redis-namespace (>= 1.5.2)
......@@ -928,7 +928,7 @@ DEPENDENCIES
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0)
sidekiq (~> 4.2)
sidekiq (~> 4.2.7)
sidekiq-cron (~> 0.4.4)
sidekiq-limit_fetch (~> 3.4)
simplecov (= 0.12.0)
......
......@@ -3,7 +3,7 @@
this.CommitFile = (function() {
function CommitFile(file) {
if ($('.image', file).length) {
new ImageFile(file);
new gl.ImageFile(file);
}
}
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, padded-blocks, max-len */
(function() {
this.ImageFile = (function() {
gl.ImageFile = (function() {
var prepareFrames;
// Width where images must fits in, for 2-up this gets divided by 2
......
......@@ -6,7 +6,7 @@
var genericError, genericSuccess, showTooltip;
genericSuccess = function(e) {
showTooltip(e.trigger, 'Copied!');
showTooltip(e.trigger, 'Copied');
// Clear the selection and blur the trigger so it loses its border
e.clearSelection();
return $(e.trigger).blur();
......@@ -31,7 +31,7 @@
var originalTitle = $target.data('original-title');
$target
.attr('title', 'Copied!')
.attr('title', 'Copied')
.tooltip('fixTitle')
.tooltip('show')
.attr('title', originalTitle)
......
......@@ -5,8 +5,11 @@
class Diff {
constructor() {
$('.files .diff-file').singleFileDiff();
$('.files .diff-file').filesCommentButton();
const $diffFile = $('.files .diff-file');
$diffFile.singleFileDiff();
$diffFile.filesCommentButton();
$diffFile.each((index, file) => new gl.ImageFile(file));
if (this.diffViewType() === 'parallel') {
$('.content-wrapper .container-fluid').removeClass('container-limited');
......
......@@ -52,6 +52,10 @@
return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
},
beforeInsert: function(value) {
if (value && !this.setting.skipSpecialCharacterTest) {
var withoutAt = value.substring(1);
if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"';
}
if (!GitLab.GfmAutoComplete.dataLoaded) {
return this.at;
} else {
......@@ -117,6 +121,7 @@
insertTpl: ':${name}:',
data: ['loading'],
startWithSpace: false,
skipSpecialCharacterTest: true,
callbacks: {
sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter,
......@@ -141,6 +146,7 @@
data: ['loading'],
startWithSpace: false,
alwaysHighlightFirst: true,
skipSpecialCharacterTest: true,
callbacks: {
sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter,
......@@ -219,12 +225,13 @@
}
};
})(this),
insertTpl: '${atwho-at}"${title}"',
insertTpl: '${atwho-at}${title}',
data: ['loading'],
startWithSpace: false,
callbacks: {
matcher: this.DefaultOptions.matcher,
sorter: this.DefaultOptions.sorter,
beforeInsert: this.DefaultOptions.beforeInsert,
beforeSave: function(milestones) {
return $.map(milestones, function(m) {
if (m.title == null) {
......@@ -284,18 +291,11 @@
callbacks: {
matcher: this.DefaultOptions.matcher,
sorter: this.DefaultOptions.sorter,
beforeInsert: this.DefaultOptions.beforeInsert,
beforeSave: function(merges) {
var sanitizeLabelTitle;
sanitizeLabelTitle = function(title) {
if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) {
return "\"" + (sanitize(title)) + "\"";
} else {
return sanitize(title);
}
};
return $.map(merges, function(m) {
return {
title: sanitizeLabelTitle(m.title),
title: sanitize(m.title),
color: m.color,
search: "" + m.title
};
......@@ -308,6 +308,7 @@
at: '/',
alias: 'commands',
searchKey: 'search',
skipSpecialCharacterTest: true,
displayTpl: function(value) {
var tpl = '<li>/${name}';
if (value.aliases.length > 0) {
......
......@@ -2,7 +2,7 @@
(function() {
window.ContributorsStatGraphUtil = {
parse_log: function(log) {
var by_author, by_email, data, entry, i, len, total;
var by_author, by_email, data, entry, i, len, total, normalized_email;
total = {};
by_author = {};
by_email = {};
......@@ -11,7 +11,8 @@
if (total[entry.date] == null) {
this.add_date(entry.date, total);
}
data = by_author[entry.author_name] || by_email[entry.author_email];
normalized_email = entry.author_email.toLowerCase();
data = by_author[entry.author_name] || by_email[normalized_email];
if (data == null) {
data = this.add_author(entry, by_author, by_email);
}
......@@ -32,12 +33,14 @@
return collection[date].date = date;
},
add_author: function(author, by_author, by_email) {
var data;
var data, normalized_email;
data = {};
data.author_name = author.author_name;
data.author_email = author.author_email;
normalized_email = author.author_email.toLowerCase();
by_author[author.author_name] = data;
return by_email[author.author_email] = data;
by_email[normalized_email] = data;
return data;
},
store_data: function(entry, total, by_author) {
this.store_commits(total, by_author);
......
......@@ -40,19 +40,26 @@
$('#modal_merge_info').modal({
show: false
});
this.firstCICheck = true;
this.readyForCICheck = false;
this.readyForCIEnvironmentCheck = false;
this.cancel = false;
clearInterval(this.fetchBuildStatusInterval);
clearInterval(this.fetchBuildEnvironmentStatusInterval);
this.clearEventListeners();
this.addEventListeners();
this.getCIStatus(false);
this.getCIEnvironmentsStatus();
this.retrieveSuccessIcon();
this.pollCIStatus();
this.pollCIEnvironmentsStatus();
this.ciStatusInterval = new global.SmartInterval({
callback: this.getCIStatus.bind(this, true),
startingInterval: 10000,
maxInterval: 30000,
hiddenInterval: 120000,
incrementByFactorOf: 5000,
});
this.ciEnvironmentStatusInterval = new global.SmartInterval({
callback: this.getCIEnvironmentsStatus.bind(this),
startingInterval: 30000,
maxInterval: 120000,
hiddenInterval: 240000,
incrementByFactorOf: 15000,
immediateExecution: true,
});
notifyPermissions();
}
......@@ -60,10 +67,6 @@
return $(document).off('page:change.merge_request');
};
MergeRequestWidget.prototype.cancelPolling = function() {
return this.cancel = true;
};
MergeRequestWidget.prototype.addEventListeners = function() {
var allowedPages;
allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes'];
......@@ -72,9 +75,6 @@
var page;
page = $('body').data('page').split(':').last();
if (allowedPages.indexOf(page) < 0) {
clearInterval(_this.fetchBuildStatusInterval);
clearInterval(_this.fetchBuildEnvironmentStatusInterval);
_this.cancelPolling();
return _this.clearEventListeners();
}
};
......@@ -101,7 +101,7 @@
urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
return window.location.href = window.location.pathname + urlSuffix;
} else if (data.merge_error) {
return this.$widgetBody.html("<h4>" + data.merge_error + "</h4>");
return _this.$widgetBody.html("<h4>" + data.merge_error + "</h4>");
} else {
callback = function() {
return merge_request_widget.mergeInProgress(deleteSourceBranch);
......@@ -114,6 +114,11 @@
});
};
MergeRequestWidget.prototype.cancelPolling = function () {
this.ciStatusInterval.cancel();
this.ciEnvironmentStatusInterval.cancel();
};
MergeRequestWidget.prototype.getMergeStatus = function() {
return $.get(this.opts.merge_check_url, function(data) {
return $('.mr-state-widget').replaceWith(data);
......@@ -131,18 +136,6 @@
}
};
MergeRequestWidget.prototype.pollCIStatus = function() {
return this.fetchBuildStatusInterval = setInterval(((function(_this) {
return function() {
if (!_this.readyForCICheck) {
return;
}
_this.getCIStatus(true);
return _this.readyForCICheck = false;
};
})(this)), 10000);
};
MergeRequestWidget.prototype.getCIStatus = function(showNotification) {
var _this;
_this = this;
......@@ -150,23 +143,17 @@
return $.getJSON(this.opts.ci_status_url, (function(_this) {
return function(data) {
var message, status, title;
if (_this.cancel) {
return;
}
_this.readyForCICheck = true;
if (data.status === '') {
return;
}
if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
if (_this.firstCICheck || data.status !== _this.opts.ci_status && (data.status != null)) {
if (data.status !== _this.opts.ci_status && (data.status != null)) {
_this.opts.ci_status = data.status;
_this.showCIStatus(data.status);
if (data.coverage) {
_this.showCICoverage(data.coverage);
}
// The first check should only update the UI, a notification
// should only be displayed on status changes
if (showNotification && !_this.firstCICheck) {
if (showNotification) {
status = _this.ciLabelForStatus(data.status);
if (status === "preparing") {
title = _this.opts.ci_title.preparing;
......@@ -184,24 +171,13 @@
return Turbolinks.visit(_this.opts.builds_path);
});
}
return _this.firstCICheck = false;
}
};
})(this));
};
MergeRequestWidget.prototype.pollCIEnvironmentsStatus = function() {
this.fetchBuildEnvironmentStatusInterval = setInterval(() => {
if (!this.readyForCIEnvironmentCheck) return;
this.getCIEnvironmentsStatus();
this.readyForCIEnvironmentCheck = false;
}, 300000);
};
MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() {
$.getJSON(this.opts.ci_environments_status_url, (environments) => {
if (this.cancel) return;
this.readyForCIEnvironmentCheck = true;
if (environments && environments.length) this.renderEnvironments(environments);
});
};
......@@ -212,11 +188,11 @@
if ($(`.mr-state-widget #${ environment.id }`).length) return;
const $template = $(DEPLOYMENT_TEMPLATE);
if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove();
if (!environment.stop_url) {
$('.js-stop-env-link', $template).remove();
}
if (environment.deployed_at && environment.deployed_at_formatted) {
environment.deployed_at = gl.utils.getTimeago().format(environment.deployed_at, 'gl_en') + '.';
} else {
......
......@@ -4,7 +4,7 @@
((global) => {
class Pipelines {
constructor(options) {
constructor(options = {}) {
if (options.initTabs && options.tabsOptions) {
new global.LinkedTabs(options.tabsOptions);
......@@ -14,9 +14,11 @@
}
addMarginToBuildColumns() {
this.pipelineGraph = document.querySelector('.pipeline-graph');
const secondChildBuildNodes = document.querySelector('.pipeline-graph').querySelectorAll('.build:nth-child(2)');
for (buildNodeIndex in secondChildBuildNodes) {
this.pipelineGraph = document.querySelector('.js-pipeline-graph');
const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)');
for (const buildNodeIndex in secondChildBuildNodes) {
const buildNode = secondChildBuildNodes[buildNodeIndex];
const firstChildBuildNode = buildNode.previousElementSibling;
if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue;
......@@ -28,6 +30,7 @@
const columnBuilds = previousColumn.querySelectorAll('.build');
if (columnBuilds.length === 1) previousColumn.classList.add('no-margin');
}
this.pipelineGraph.classList.remove('hidden');
}
}
......
......@@ -7,24 +7,31 @@
(() => {
class SmartInterval {
/**
* @param { function } callback Function to be called on each iteration (required)
* @param { milliseconds } startingInterval `currentInterval` is set to this initially
* @param { milliseconds } maxInterval `currentInterval` will be incremented to this
* @param { integer } incrementByFactorOf `currentInterval` is incremented by this factor
* @param { boolean } lazyStart Configure if timer is initialized on instantiation or lazily
* @param { function } opts.callback Function to be called on each iteration (required)
* @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially
* @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this
* @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this
* when the page is hidden
* @param { integer } opts.incrementByFactorOf `currentInterval` is incremented by this factor
* @param { boolean } opts.lazyStart Configure if timer is initialized on
* instantiation or lazily
* @param { boolean } opts.immediateExecution Configure if callback should
* be executed before the first interval.
*/
constructor({ callback, startingInterval, maxInterval, incrementByFactorOf, lazyStart }) {
constructor(opts = {}) {
this.cfg = {
callback,
startingInterval,
maxInterval,
incrementByFactorOf,
lazyStart,
callback: opts.callback,
startingInterval: opts.startingInterval,
maxInterval: opts.maxInterval,
hiddenInterval: opts.hiddenInterval,
incrementByFactorOf: opts.incrementByFactorOf,
lazyStart: opts.lazyStart,
immediateExecution: opts.immediateExecution,
};
this.state = {
intervalId: null,
currentInterval: startingInterval,
currentInterval: this.cfg.startingInterval,
pageVisibility: 'visible',
};
......@@ -36,6 +43,11 @@
const cfg = this.cfg;
const state = this.state;
if (cfg.immediateExecution) {
cfg.immediateExecution = false;
cfg.callback();
}
state.intervalId = window.setInterval(() => {
cfg.callback();
......@@ -54,14 +66,29 @@
this.stopTimer();
}
onVisibilityHidden() {
if (this.cfg.hiddenInterval) {
this.setCurrentInterval(this.cfg.hiddenInterval);
this.resume();
} else {
this.cancel();
}
}
// start a timer, using the existing interval
resume() {
this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped
this.start();
}
onVisibilityVisible() {
this.cancel();
this.start();
}
destroy() {
this.cancel();
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
$(document).off('visibilitychange').off('page:before-unload');
}
......@@ -80,11 +107,7 @@
initVisibilityChangeHandling() {
// cancel interval when tab no longer shown (prevents cached pages from polling)
$(document)
.off('visibilitychange').on('visibilitychange', (e) => {
this.state.pageVisibility = e.target.visibilityState;
this.handleVisibilityChange();
});
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
}
initPageUnloadHandling() {
......@@ -92,10 +115,11 @@
$(document).on('page:before-unload', () => this.cancel());
}
handleVisibilityChange() {
const state = this.state;
const intervalAction = state.pageVisibility === 'hidden' ? this.cancel : this.resume;
handleVisibilityChange(e) {
this.state.pageVisibility = e.target.visibilityState;
const intervalAction = this.isPageVisible() ?
this.onVisibilityVisible :
this.onVisibilityHidden;
intervalAction.apply(this);
}
......@@ -111,6 +135,7 @@
incrementInterval() {
const cfg = this.cfg;
const currentInterval = this.getCurrentInterval();
if (cfg.hiddenInterval && !this.isPageVisible()) return;
let nextInterval = currentInterval * cfg.incrementByFactorOf;
if (nextInterval > cfg.maxInterval) {
......@@ -120,6 +145,8 @@
this.setCurrentInterval(nextInterval);
}
isPageVisible() { return this.state.pageVisibility === 'visible'; }
stopTimer() {
const state = this.state;
......
......@@ -486,6 +486,7 @@ $jq-ui-default-color: #777;
$label-gray-bg: #f8fafc;
$label-inverse-bg: #333;
$label-remove-border: rgba(0, 0, 0, .1);
$label-border-radius: 14px;
/*
* Lint
......
......@@ -30,6 +30,7 @@
.color-label {
padding: 6px 10px;
border-radius: $label-border-radius;
}
}
......
......@@ -104,7 +104,8 @@
}
.color-label {
padding: 3px 4px;
padding: 3px 7px;
border-radius: $label-border-radius;
}
.dropdown-labels-error {
......
.notification-list-item {
line-height: 34px;
.dropdown-menu {
@extend .dropdown-menu-align-right;
}
}
.notification {
......
......@@ -188,6 +188,10 @@
margin-left: 10px;
}
.notification-dropdown .dropdown-menu {
@extend .dropdown-menu-align-right;
}
.download-button {
@media (max-width: $screen-md-max) {
margin-left: 0;
......
.snippet-row {
.title {
margin-bottom: 2px;
}
.snippet-filename {
padding: 0 2px;
}
}
.snippet-form-holder .file-holder .file-title {
padding: 2px;
}
......@@ -24,11 +34,17 @@
padding-bottom: $gl-padding;
}
.snippet-header {
padding: $gl-padding 0;
}
.snippet-title {
font-size: 24px;
font-weight: 600;
padding: $gl-padding;
padding-left: 0;
}
.snippet-edited-ago {
color: $gray-darkest;
}
.snippet-actions {
......
......@@ -10,7 +10,14 @@ class Projects::ReleasesController < Projects::ApplicationController
end
def update
release.update_attributes(release_params)
# Release belongs to Tag which is not active record object,
# it exists only to save a description to each Tag.
# If description is empty we should destroy the existing record.
if release_params[:description].present?
release.update_attributes(release_params)
else
release.destroy
end
redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name)
end
......
......@@ -19,10 +19,12 @@ class Projects::SnippetsController < Projects::ApplicationController
respond_to :html
def index
@snippets = SnippetsFinder.new.execute(current_user, {
@snippets = SnippetsFinder.new.execute(
current_user,
filter: :by_project,
project: @project
})
project: @project,
scope: params[:scope]
)
@snippets = @snippets.page(params[:page])
end
......
......@@ -37,6 +37,12 @@ class SessionsController < Devise::SessionsController
end
end
def destroy
super
# hide the signed_out notice
flash[:notice] = nil
end
private
# Handle an "initial setup" state, where there's only one user, it's an admin,
......
class SnippetsFinder
def execute(current_user, params = {})
filter = params[:filter]
user = params.fetch(:user, current_user)
case filter
when :all then
snippets(current_user).fresh
when :public then
Snippet.are_public.fresh
when :by_user then
by_user(current_user, params[:user], params[:scope])
by_user(current_user, user, params[:scope])
when :by_project
by_project(current_user, params[:project])
by_project(current_user, params[:project], params[:scope])
end
end
......@@ -29,35 +32,35 @@ class SnippetsFinder
def by_user(current_user, user, scope)
snippets = user.snippets.fresh
return snippets.are_public unless current_user
if user == current_user
case scope
when 'are_internal' then
snippets.are_internal
when 'are_private' then
snippets.are_private
when 'are_public' then
snippets.are_public
else
snippets
end
if current_user
include_private = user == current_user
by_scope(snippets, scope, include_private)
else
snippets.public_and_internal
snippets.are_public
end
end
def by_project(current_user, project)
def by_project(current_user, project, scope)
snippets = project.snippets.fresh
if current_user
if project.team.member?(current_user) || current_user.admin?
snippets
else
snippets.public_and_internal
end
include_private = project.team.member?(current_user) || current_user.admin?
by_scope(snippets, scope, include_private)
else
snippets.are_public
end
end
def by_scope(snippets, scope = nil, include_private = false)
case scope.to_s
when 'are_private'
include_private ? snippets.are_private : Snippet.none
when 'are_internal'
snippets.are_internal
when 'are_public'
snippets.are_public
else
include_private ? snippets : snippets.public_and_internal
end
end
end
......@@ -16,7 +16,7 @@ module ButtonHelper
# See http://clipboardjs.com/#usage
def clipboard_button(data = {})
css_class = data[:class] || 'btn-clipboard btn-transparent'
title = data[:title] || 'Copy to Clipboard'
title = data[:title] || 'Copy to clipboard'
data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
content_tag :button,
icon('clipboard'),
......
......@@ -159,6 +159,11 @@ module GitlabRoutingHelper
resend_invite_namespace_project_project_member_path(project_member.source.namespace, project_member.source, project_member)
end
# Snippets
def personal_snippet_url(snippet, *args)
snippet_url(snippet)
end
# Groups
## Members
......
......@@ -8,6 +8,17 @@ module SnippetsHelper
end
end
# Return the path of a snippets index for a user or for a project
#
# @returns String, path to snippet index
def subject_snippets_path(subject = nil, opts = nil)
if subject.is_a?(Project)
namespace_project_snippets_path(subject.namespace, subject, opts)
else # assume subject === User
dashboard_snippets_path(opts)
end
end
# Get an array of line numbers surrounding a matching
# line, bounded by min/max.
#
......
......@@ -7,6 +7,7 @@ module Routable
has_one :route, as: :source, autosave: true, dependent: :destroy
validates_associated :route
validates :route, presence: true
before_validation :update_route_path, if: :full_path_changed?
end
......@@ -28,17 +29,17 @@ module Routable
order_sql = "(CASE WHEN #{binary} routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)"
where_paths_in([path]).reorder(order_sql).take
where_full_path_in([path]).reorder(order_sql).take
end
# Builds a relation to find multiple objects by their full paths.
#
# Usage:
#
# Klass.where_paths_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee})
# Klass.where_full_path_in(%w{gitlab-org/gitlab-ce gitlab-org/gitlab-ee})
#
# Returns an ActiveRecord::Relation.
def where_paths_in(paths)
def where_full_path_in(paths)
wheres = []
cast_lower = Gitlab::Database.postgresql?
......
......@@ -3,7 +3,8 @@ require "addressable/uri"
class BuildkiteService < CiService
ENDPOINT = "https://buildkite.com"
prop_accessor :project_url, :token, :enable_ssl_verification
prop_accessor :project_url, :token
boolean_accessor :enable_ssl_verification
validates :project_url, presence: true, url: true, if: :activated?
validates :token, presence: true, if: :activated?
......
class DroneCiService < CiService
prop_accessor :drone_url, :token, :enable_ssl_verification
prop_accessor :drone_url, :token
boolean_accessor :enable_ssl_verification
validates :drone_url, presence: true, url: true, if: :activated?
validates :token, presence: true, if: :activated?
......
class EmailsOnPushService < Service
prop_accessor :send_from_committer_email
prop_accessor :disable_diffs
boolean_accessor :send_from_committer_email
boolean_accessor :disable_diffs
prop_accessor :recipients
validates :recipients, presence: true, if: :activated?
......@@ -24,20 +24,20 @@ class EmailsOnPushService < Service
return unless supported_events.include?(push_data[:object_kind])
EmailsOnPushWorker.perform_async(
project_id,
recipients,
push_data,
send_from_committer_email: send_from_committer_email?,
disable_diffs: disable_diffs?
project_id,
recipients,
push_data,
send_from_committer_email: send_from_committer_email?,
disable_diffs: disable_diffs?
)
end
def send_from_committer_email?
self.send_from_committer_email == "1"
Gitlab::Utils.to_boolean(self.send_from_committer_email)
end
def disable_diffs?
self.disable_diffs == "1"
Gitlab::Utils.to_boolean(self.disable_diffs)
end
def fields
......
......@@ -8,8 +8,8 @@ class HipchatService < Service
ul ol li dl dt dd
]
prop_accessor :token, :room, :server, :notify, :color, :api_version
boolean_accessor :notify_only_broken_builds
prop_accessor :token, :room, :server, :color, :api_version
boolean_accessor :notify_only_broken_builds, :notify
validates :token, presence: true, if: :activated?
def initialize_properties
......@@ -75,7 +75,7 @@ class HipchatService < Service
end
def message_options(data = nil)
{ notify: notify.present? && notify == '1', color: message_color(data) }
{ notify: notify.present? && Gitlab::Utils.to_boolean(notify), color: message_color(data) }
end
def create_message(data)
......
......@@ -2,7 +2,8 @@ require 'uri'
class IrkerService < Service
prop_accessor :server_host, :server_port, :default_irc_uri
prop_accessor :colorize_messages, :recipients, :channels
prop_accessor :recipients, :channels
boolean_accessor :colorize_messages
validates :recipients, presence: true, if: :activated?
before_validation :get_channels
......
......@@ -220,7 +220,7 @@ class JiraService < IssueTrackerService
entity_title = data[:entity][:title]
project_name = data[:project][:name]
message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'"
message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title.chomp}'"
link_title = "GitLab: Mentioned on #{entity_name} - #{entity_title}"
link_props = build_remote_link_props(url: entity_url, title: link_title)
......
......@@ -304,10 +304,6 @@ class User < ActiveRecord::Base
personal_access_token.user if personal_access_token
end
def by_username_or_id(name_or_id)
find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i)
end
# Returns a user for the given SSH key.
def find_by_ssh_key_id(key_id)
find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
......
......@@ -6,9 +6,14 @@ class PersonalSnippetPolicy < BasePolicy
if @subject.author == @user
can! :read_personal_snippet
can! :update_personal_snippet
can! :destroy_personal_snippet
can! :admin_personal_snippet
end
unless @user.external?
can! :create_personal_snippet
end
if @subject.internal? && !@user.external?
can! :read_personal_snippet
end
......
......@@ -35,7 +35,7 @@ module Commits
success
else
error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
It may have already been #{action.to_s.dasherize}, or a more recent commit may have updated some of its content."
A #{action.to_s.dasherize} may have already been performed with this #{@commit.change_type_title(current_user)}, or a more recent commit may have updated some of its content."
raise ChangeError, error_msg
end
end
......
%ul.nav-links
= nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
= link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
Your Snippets
= nav_link(page: explore_snippets_path) do
= link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do
Explore Snippets
.top-area
%ul.nav-links
= nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
= link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
Your Snippets
= nav_link(page: explore_snippets_path) do
= link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do
Explore Snippets
- if current_user
.nav-controls.hidden-xs
= link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do
New snippet
......@@ -2,41 +2,11 @@
- header_title "Snippets", dashboard_snippets_path
= render 'dashboard/snippets_head'
= render partial: 'snippets/snippets_scope_menu', locals: { include_private: true }
.nav-block
.controls.hidden-xs
= link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do
= icon('plus')
New snippet
.visible-xs
&nbsp;
= link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do
New snippet
.nav-links.snippet-scope-menu
%li{ class: ("active" unless params[:scope]) }
= link_to dashboard_snippets_path do
All
%span.badge
= current_user.snippets.count
%li{ class: ("active" if params[:scope] == "are_private") }
= link_to dashboard_snippets_path(scope: 'are_private') do
Private
%span.badge
= current_user.snippets.are_private.count
%li{ class: ("active" if params[:scope] == "are_internal") }
= link_to dashboard_snippets_path(scope: 'are_internal') do
Internal
%span.badge
= current_user.snippets.are_internal.count
%li{ class: ("active" if params[:scope] == "are_public") }
= link_to dashboard_snippets_path(scope: 'are_public') do
Public
%span.badge
= current_user.snippets.are_public.count
.visible-xs
= link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do
= icon('plus')
New snippet
= render 'snippets/snippets'
= render partial: 'snippets/snippets', locals: { link_project: true }
......@@ -6,12 +6,4 @@
- else
= render 'explore/head'
.row-content-block
- if current_user
= link_to new_snippet_path, class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do
New snippet
.oneline
Public snippets created by you and other users are listed here
= render 'snippets/snippets'
= render partial: 'snippets/snippets', locals: { link_project: true }
......@@ -6,13 +6,13 @@
- if group_issues(@group).exists?
.top-area
= render 'shared/issuable/nav', type: :issues
.nav-controls
- if current_user
- if current_user
.nav-controls
= link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do
= icon('rss')
%span.icon-label
Subscribe
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
= render 'shared/issuable/filter', type: :issues
......
......@@ -2,8 +2,9 @@
.top-area
= render 'shared/issuable/nav', type: :merge_requests
.nav-controls
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
- if current_user
.nav-controls
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
= render 'shared/issuable/filter', type: :merge_requests
......
- page_title @path.split("/").reverse.map(&:humanize)
.documentation.wiki
= markdown @markdown.gsub('$your_email', current_user.try(:email) || "email@example.com")
= markdown @markdown
- if current_user
- can_admin_group = can?(current_user, :admin_group, @group)
- can_edit = can?(current_user, :admin_group, @group)
- member = @group.members.find_by(user_id: current_user.id)
- can_leave = member && can?(current_user, :destroy_group_member, member)
- if can_admin_group || can_edit || can_leave
- if can_admin_group || can_edit
.controls
.dropdown.group-settings-dropdown
%a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'}
......@@ -14,13 +12,7 @@
- if can_admin_group
= nav_link(path: 'groups#projects') do
= link_to 'Projects', projects_group_path(@group), title: 'Projects'
- if (can_edit || can_leave) && can_admin_group
- if can_edit && can_admin_group
%li.divider
- if can_edit
%li
= link_to 'Edit Group', edit_group_path(@group)
- if can_leave
%li
= link_to polymorphic_path([:leave, @group, :members]),
data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do
Leave Group
......@@ -6,23 +6,14 @@
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- can_edit = can?(current_user, :admin_project, @project)
-# We don't use @project.team.find_member because it searches for group members too...
- member = @project.members.find_by(user_id: current_user.id)
- can_leave = member && can?(current_user, :destroy_project_member, member)
= render 'layouts/nav/project_settings', can_edit: can_edit
- if can_edit || can_leave
- if can_edit
%li.divider
- if can_edit
%li
= link_to edit_project_path(@project) do
Edit Project
- if can_leave
%li
= link_to polymorphic_path([:leave, @project, :members]),
data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do
Leave Project
%li
= link_to edit_project_path(@project) do
Edit Project
.scrolling-tabs-container{ class: nav_control_class }
.fade-left
......
......@@ -30,7 +30,7 @@
%br
.clearfix
.form-group.pull-left.global-notification-setting
= render 'shared/notifications/button', notification_setting: @global_notification_setting, left_align: true
= render 'shared/notifications/button', notification_setting: @global_notification_setting
.clearfix
......
- is_playable = subject.playable? && can?(current_user, :update_build, @project)
- if is_playable
= link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.pipeline-graph', placement: 'bottom' } do
= link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.js-pipeline-graph', placement: 'bottom' } do
= ci_icon_for_status('play')
.ci-status-text= subject.name
- elsif can?(current_user, :read_build, @project)
= link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } do
= link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } do
%span{class: "ci-status-icon ci-status-icon-#{subject.status}"}
= ci_icon_for_status(subject.status)
.ci-status-text= subject.name
......
......@@ -13,7 +13,7 @@
%a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title== #{label} this #{commit.change_type_title(current_user)}
.modal-body
= form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-#{type}-form js-requires-input' do
= form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do
.form-group.branch
= label_tag 'target_branch', target_label, class: 'control-label'
.col-sm-10
......@@ -23,12 +23,11 @@
- if can?(current_user, :push_code, @project)
.js-create-merge-request-container
.checkbox
- nonce = SecureRandom.hex
= label_tag "create_merge_request-#{nonce}" do
= check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}"
= label_tag do
= check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: nil
Start a <strong>new merge request</strong> with these changes
- else
= hidden_field_tag 'create_merge_request', 1
= hidden_field_tag 'create_merge_request', 1, id: nil
.form-actions
= submit_tag label, class: 'btn btn-create'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
......
.page-content-header
.header-main-content
%strong Commit
%strong.monospace.js-details-short= @commit.short_id
= link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
%span.text-expander
\...
%span.js-details-content.hide
%strong.monospace.commit-hash-full= @commit.id
%strong
= clipboard_button(clipboard_text: @commit.id)
= @commit.short_id
%span.hidden-xs authored
#{time_ago_with_tooltip(@commit.authored_date)}
%span by
......
......@@ -24,7 +24,7 @@
in
= time_interval_in_words pipeline.duration
.row-content-block.build-content.middle-block.hidden
.row-content-block.build-content.middle-block.js-pipeline-graph.hidden
= render "projects/pipelines/graph", pipeline: pipeline
- if pipeline.yaml_errors.present?
......
......@@ -146,7 +146,7 @@
such as compressing file revisions and removing unreachable objects.
.col-lg-9
= link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project),
method: :post, class: "btn btn-save"
method: :post, class: "btn btn-default"
%hr
.row.prepend-top-default
.col-lg-3
......
%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } }
%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } }
- if subject.target_url
= link_to subject.target_url do
%span{class: "ci-status-icon ci-status-icon-#{subject.status}"}
......
......@@ -65,7 +65,7 @@
.note-text.md
= preserve do
= note.redacted_note_html
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note_editable
= render 'projects/notes/edit_form', note: note
.note-awards
......
......@@ -12,7 +12,7 @@
.tab-content
#js-tab-pipeline.tab-pane
.build-content.middle-block
.build-content.middle-block.js-pipeline-graph
= render "projects/pipelines/graph", pipeline: pipeline
#js-tab-builds.tab-pane
......
.hidden-xs
- if can?(current_user, :create_project_snippet, @project)
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New snippet" do
New snippet
- if can?(current_user, :update_project_snippet, @snippet)
= link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do
Delete
- if can?(current_user, :update_project_snippet, @snippet)
= link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do
= link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped" do
Edit
- if can?(current_user, :update_project_snippet, @snippet)
= link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do
Delete
- if can?(current_user, :create_project_snippet, @project)
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do
New snippet
- if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet)
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
......
- page_title "Snippets"
.sub-header-block
- if can?(current_user, :create_project_snippet, @project)
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do
New snippet
- if current_user
.top-area
- include_private = @project.team.member?(current_user) || current_user.admin?
= render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private }
.nav-controls.hidden-xs
- if can?(current_user, :create_project_snippet, @project)
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New snippet" do
New snippet
.oneline
Share code pastes with others out of git repository
- if can?(current_user, :create_project_snippet, @project)
.visible-xs
&nbsp;
= link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-block", title: "New snippet" do
New snippet
= render 'snippets/snippets'
......@@ -5,14 +5,11 @@
%tr
%th Name
%th.hidden-xs
.pull-left Last Commit
.pull-left Last commit
.last-commit.hidden-sm.pull-left
&nbsp;
%i.fa.fa-angle-right
&nbsp;
%small.light
%small.light
= clipboard_button(clipboard_text: @commit.id)
= link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
&ndash;
= time_ago_with_tooltip(@commit.committed_date)
= @commit.full_title
%small.commit-history-link-spacer &#124;
......
......@@ -14,7 +14,7 @@
= link_to snippet_title.project.name_with_namespace, namespace_project_path(snippet_title.project.namespace, snippet_title.project)
.snippet-info
= "##{snippet_title.id}"
= snippet_title.to_reference
%span
by
= link_to user_snippets_path(snippet_title.author) do
......
- if can?(current_user, :request_access, source)
- if requester = source.requesters.find_by(user_id: current_user.id)
= link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
method: :delete,
data: { confirm: remove_member_message(requester) },
class: 'btn'
- else
= link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
method: :post,
class: 'btn'
- model_name = source.model_name.to_s.downcase
- if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id))
= link_to "Leave #{model_name}", polymorphic_path([:leave, source, :members]),
method: :delete,
data: { confirm: leave_confirmation_message(source) },
class: 'btn'
- elsif requester = source.requesters.find_by(user_id: current_user.id)
= link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
method: :delete,
data: { confirm: remove_member_message(requester) },
class: 'btn'
- elsif source.request_access_enabled && can?(current_user, :request_access, source)
= link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
method: :post,
class: 'btn'
- left_align = local_assigns[:left_align]
- if notification_setting
.dropdown.notification-dropdown
= form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
......@@ -19,7 +18,7 @@
= notification_title(notification_setting.level)
= icon("caret-down")
= render "shared/notifications/notification_dropdown", notification_setting: notification_setting, left_align: left_align
= render "shared/notifications/notification_dropdown", notification_setting: notification_setting
= content_for :scripts_body do
= render "shared/notifications/custom_notifications", notification_setting: notification_setting
- left_align = local_assigns[:left_align]
%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting), ("dropdown-menu-align-right" unless left_align)] }
%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting)] }
- NotificationSetting.levels.each_key do |level|
- next if level == "custom"
- next if level == "global" && notification_setting.source.nil?
......
......@@ -8,10 +8,6 @@
%span.creator
authored
= time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago')
- if @snippet.updated_at != @snippet.created_at
%span
= icon('edit', title: 'edited')
= time_ago_with_tooltip(@snippet.updated_at, placement: 'bottom', html_class: 'snippet_edited_ago')
by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")}
.snippet-actions
......@@ -20,5 +16,9 @@
- else
= render "snippets/actions"
%h2.snippet-title.prepend-top-0.append-bottom-0
= markdown_field(@snippet, :title)
.snippet-header
%h2.snippet-title.prepend-top-0.append-bottom-0
= markdown_field(@snippet, :title)
- if @snippet.updated_at != @snippet.created_at
= edited_time_ago_with_tooltip(@snippet, placement: 'bottom', html_class: 'snippet-edited-ago')
- link_project = local_assigns.fetch(:link_project, false)
%li.snippet-row
= image_tag avatar_icon(snippet.author_email), class: "avatar s40 hidden-xs", alt: ''
.title
= link_to reliable_snippet_path(snippet) do
= snippet.title
- if snippet.private?
%span.label.label-gray.hidden-xs
= icon('lock')
private
%span.monospace.pull-right.hidden-xs
= snippet.file_name
- if snippet.file_name
%span.snippet-filename.monospace.hidden-xs
= snippet.file_name
%ul.controls.visible-xs
%ul.controls
%li
- note_count = snippet.notes.user.count
= link_to reliable_snippet_path(snippet, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
......@@ -22,11 +21,17 @@
= visibility_level_label(snippet.visibility_level)
= visibility_level_icon(snippet.visibility_level, fw: false)
%small.pull-right.cgray.hidden-xs
- if snippet.project_id?
= link_to snippet.project.name_with_namespace, namespace_project_path(snippet.project.namespace, snippet.project)
.snippet-info.hidden-xs
.snippet-info
#{snippet.to_reference} &middot;
authored #{time_ago_with_tooltip(snippet.created_at, placement: 'bottom', html_class: 'snippet-created-ago')}
by
= link_to user_snippets_path(snippet.author) do
= snippet.author_name
authored #{time_ago_with_tooltip(snippet.created_at)}
- if link_project && snippet.project_id?
%span.hidden-xs
in
= link_to namespace_project_path(snippet.project.namespace, snippet.project) do
= snippet.project.name_with_namespace
.pull-right.snippet-updated-at
%span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom')}
.hidden-xs
- if current_user
= link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New snippet" do
New snippet
- if can?(current_user, :admin_personal_snippet, @snippet)
= link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do
Delete
- if can?(current_user, :update_personal_snippet, @snippet)
= link_to edit_snippet_path(@snippet), class: "btn btn-grouped snippable-edit" do
= link_to edit_snippet_path(@snippet), class: "btn btn-grouped" do
Edit
- if can?(current_user, :admin_personal_snippet, @snippet)
= link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do
Delete
- if current_user
= link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do
New snippet
- if current_user
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
......
- remote = local_assigns.fetch(:remote, false)
- link_project = local_assigns.fetch(:link_project, false)
.snippets-list-holder
%ul.content-list
= render partial: 'shared/snippets/snippet', collection: @snippets
= render partial: 'shared/snippets/snippet', collection: @snippets, locals: { link_project: link_project }
- if @snippets.empty?
%li
.nothing-here-block Nothing here.
......
- subject = local_assigns.fetch(:subject, current_user)
- include_private = local_assigns.fetch(:include_private, false)
.nav-links.snippet-scope-menu
%li{ class: ("active" unless params[:scope]) }
= link_to subject_snippets_path(subject) do
All
%span.badge
- if include_private
= subject.snippets.count
- else
= subject.snippets.public_and_internal.count
- if include_private
%li{ class: ("active" if params[:scope] == "are_private") }
= link_to subject_snippets_path(subject, scope: 'are_private') do
Private
%span.badge
= subject.snippets.are_private.count
%li{ class: ("active" if params[:scope] == "are_internal") }
= link_to subject_snippets_path(subject, scope: 'are_internal') do
Internal
%span.badge
= subject.snippets.are_internal.count
%li{ class: ("active" if params[:scope] == "are_public") }
= link_to subject_snippets_path(subject, scope: 'are_public') do
Public
%span.badge
= subject.snippets.are_public.count
---
title: group authors in contribution graph with case insensitive email handle comparison
merge_request: 8021
author:
---
title: Moved Leave Project and Leave Group buttons to access_request_buttons from
the settings dropdown
merge_request: 7600
author:
---
title: Use SmartInterval for MR widget and improve visibilitychange functionality
merge_request: 7762
author:
---
title: Prevent user creating issue or MR without signing in for a group
merge_request: 7902
author:
---
title: 'fix: removed signed_out notification'
merge_request: 7958
author: jnoortheen
---
title: Changed Housekeeping button on project settings page to default styling
merge_request:
author: Ryan Harris
---
title: 'API: Memoize the current_user so that sudo can work properly'
merge_request: 8017
author:
---
title: Fix TypeError: Cannot read property 'initTabs' on commit builds tab
merge_request: 8009
author:
---
title: Allow all alphanumeric characters in file names
merge_request: 8002
author: winniehell
---
title: 'API: Ability to cherry pick a commit'
merge_request: 8047
author: Robert Schilling
---
title: 'API: Simple representation of group''s projects'
merge_request: 8060
author: Robert Schilling
---
title: Replace static fixture for awards_handler_spec
merge_request: 7661
author: winniehell
---
title: For single line git commit messages, the close quote should be on the same
line as the open quote
merge_request:
author:
---
title: 'API: Endpoint to expose personal snippets as /snippets'
merge_request: 6373
author: Bernard Guyzmo Pratz
---
title: Allow to delete tag release note
merge_request:
author:
---
title: "fix display hook error message"
merge_request: 7775
author: basyura
---
title: Allow branch names with dots on API endpoint
merge_request:
author:
---
title: Avoid escaping relative links in Markdown twice
merge_request: 7940
author: winniehell
......@@ -302,7 +302,7 @@ Settings.cron_jobs['remove_expired_group_links_worker'] ||= Settingslogic.new({}
Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *'
Settings.cron_jobs['remove_expired_group_links_worker']['job_class'] = 'RemoveExpiredGroupLinksWorker'
Settings.cron_jobs['prune_old_events_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '* */6 * * *'
Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '0 */6 * * *'
Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker'
Settings.cron_jobs['trending_projects_worker'] ||= Settingslogic.new({})
......
require 'sidekiq/web'
require 'sidekiq/cron/web'
require 'api/api'
require 'constraints/group_url_constrainer'
Rails.application.routes.draw do
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddLowerPathIndexToRoutes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
return unless Gitlab::Database.postgresql?
execute 'CREATE INDEX CONCURRENTLY index_on_routes_lower_path ON routes (LOWER(path));'
end
def down
return unless Gitlab::Database.postgresql?
remove_index :routes, name: :index_on_routes_lower_path
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20161202152035) do
ActiveRecord::Schema.define(version: 20161212142807) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......
# Documentation
# GitLab Community Edition documentation
## User documentation
......
......@@ -183,6 +183,44 @@ Example response:
}
```
## Cherry pick a commit
> [Introduced][ce-8047] in GitLab 8.15.
Cherry picks a commit to a given branch.
```
POST /projects/:id/repository/commits/:sha/cherry_pick
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `sha` | string | yes | The commit hash |
| `branch` | string | yes | The name of the branch |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "branch=master" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/cherry_pick"
```
Example response:
```json
{
"id": "8b090c1b79a14f2bd9e8a738f717824ff53aebad",
"short_id": "8b090c1b",
"title": "Feature added",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
"created_at": "2016-12-12T20:10:39.000+01:00",
"committer_name": "Administrator",
"committer_email": "admin@example.com",
"message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n"
}
```
## Get the diff of a commit
Get the diff of a commit in a project.
......@@ -438,3 +476,4 @@ Example response:
```
[ce-6096]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6096 "Multi-file commit"
[ce-8047]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8047
......@@ -50,12 +50,17 @@ GET /groups/:id/projects
Parameters:
- `archived` (optional) - if passed, limit by archived status
- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
- `search` (optional) - Return list of authorized projects according to a search criteria
- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or path of a group |
| `archived` | boolean | no | Limit by archived status |
| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Return list of authorized projects matching the search criteria |
| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
Example response:
```json
[
......
......@@ -429,7 +429,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id
| `merge_request_id` | integer | yes | The ID of a project's merge request |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_request/85
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/merge_requests/85
```
## Accept MR
......
......@@ -139,6 +139,43 @@ Get Buildkite service settings for a project.
GET /projects/:id/services/buildkite
```
## Build-Emails
Get emails for GitLab CI builds.
### Create/Edit Build-Emails service
Set Build-Emails service for a project.
```
PUT /projects/:id/services/builds-email
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `recipients` | string | yes | Comma-separated list of recipient email addresses |
| `add_pusher` | boolean | no | Add pusher to recipients list |
| `notify_only_broken_builds` | boolean | no | Notify only broken builds |
### Delete Build-Emails service
Delete Build-Emails service for a project.
```
DELETE /projects/:id/services/builds-email
```
### Get Build-Emails service settings
Get Build-Emails service settings for a project.
```
GET /projects/:id/services/builds-email
```
## Campfire
Simple web-based real-time group chat
......@@ -476,12 +513,11 @@ PUT /projects/:id/services/jira
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `active` | boolean| no | Enable/disable the JIRA service. |
| `url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project, e.g., `https://jira.example.com`. |
| `project_key` | string | yes | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. |
| `username` | string | no | The username of the user created to be used with GitLab/JIRA. |
| `password` | string | no | The password of the user created to be used with GitLab/JIRA. |
| `jira_issue_transition_id` | string | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. |
| `jira_issue_transition_id` | integer | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. |
### Delete JIRA service
......@@ -491,6 +527,78 @@ Remove all previously JIRA settings from a project.
DELETE /projects/:id/services/jira
```
## Mattermost Slash Commands
Ability to receive slash commands from a Mattermost chat instance.
### Create/Edit Mattermost Slash Command service
Set Mattermost Slash Command for a project.
```
PUT /projects/:id/services/mattermost-slash-commands
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `token` | string | yes | The Mattermost token |
### Delete Mattermost Slash Command service
Delete Mattermost Slash Command service for a project.
```
DELETE /projects/:id/services/mattermost-slash-commands
```
### Get Mattermost Slash Command service settings
Get Mattermost Slash Command service settings for a project.
```
GET /projects/:id/services/mattermost-slash-commands
```
## Pipeline-Emails
Get emails for GitLab CI pipelines.
### Create/Edit Pipeline-Emails service
Set Pipeline-Emails service for a project.
```
PUT /projects/:id/services/pipelines-email
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `recipients` | string | yes | Comma-separated list of recipient email addresses |
| `add_pusher` | boolean | no | Add pusher to recipients list |
| `notify_only_broken_builds` | boolean | no | Notify only broken pipelines |
### Delete Pipeline-Emails service
Delete Pipeline-Emails service for a project.
```
DELETE /projects/:id/services/pipelines-email
```
### Get Pipeline-Emails service settings
Get Pipeline-Emails service settings for a project.
```
GET /projects/:id/services/pipelines-email
```
## PivotalTracker
Project Management Software (Source Commits Endpoint)
......
# Snippets
> [Introduced][ce-6373] in GitLab 8.15.
### Snippet visibility level
Snippets in GitLab can be either private, internal, or public.
You can set it with the `visibility_level` field in the snippet.
Constants for snippet visibility levels are:
| Visibility | Visibility level | Description |
| ---------- | ---------------- | ----------- |
| Private | `0` | The snippet is visible only to the snippet creator |
| Internal | `10` | The snippet is visible for any logged in user |
| Public | `20` | The snippet can be accessed without any authentication |
## List snippets
Get a list of current user's snippets.
```
GET /snippets
```
## Single snippet
Get a single snippet.
```
GET /snippets/:id
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | Integer | yes | The ID of a snippet |
``` bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/1
```
Example response:
``` json
{
"id": 1,
"title": "test",
"file_name": "add.rb",
"author": {
"id": 1,
"username": "john_smith",
"email": "john@example.com",
"name": "John Smith",
"state": "active",
"created_at": "2012-05-23T08:00:58Z"
},
"expires_at": null,
"updated_at": "2012-06-28T10:52:04Z",
"created_at": "2012-06-28T10:52:04Z",
"web_url": "http://example.com/snippets/1",
}
```
## Create new snippet
Creates a new snippet. The user must have permission to create new snippets.
```
POST /snippets
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `title` | String | yes | The title of a snippet |
| `file_name` | String | yes | The name of a snippet file |
| `content` | String | yes | The content of a snippet |
| `visibility_level` | Integer | yes | The snippet's visibility |
``` bash
curl --request POST --data '{"title": "This is a snippet", "content": "Hello world", "file_name": "test.txt", "visibility_level": 10 }' --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets
```
Example response:
``` json
{
"id": 1,
"title": "This is a snippet",
"file_name": "test.txt",
"author": {
"id": 1,
"username": "john_smith",
"email": "john@example.com",
"name": "John Smith",
"state": "active",
"created_at": "2012-05-23T08:00:58Z"
},
"expires_at": null,
"updated_at": "2012-06-28T10:52:04Z",
"created_at": "2012-06-28T10:52:04Z",
"web_url": "http://example.com/snippets/1",
}
```
## Update snippet
Updates an existing snippet. The user must have permission to change an existing snippet.
```
PUT /snippets/:id
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | Integer | yes | The ID of a snippet |
| `title` | String | no | The title of a snippet |
| `file_name` | String | no | The name of a snippet file |
| `content` | String | no | The content of a snippet |
| `visibility_level` | Integer | no | The snippet's visibility |
``` bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data '{"title": "foo", "content": "bar"}' https://gitlab.example.com/api/v3/snippets/1
```
Example response:
``` json
{
"id": 1,
"title": "test",
"file_name": "add.rb",
"author": {
"id": 1,
"username": "john_smith",
"email": "john@example.com",
"name": "John Smith",
"state": "active",
"created_at": "2012-05-23T08:00:58Z"
},
"expires_at": null,
"updated_at": "2012-06-28T10:52:04Z",
"created_at": "2012-06-28T10:52:04Z",
"web_url": "http://example.com/snippets/1",
}
```
## Delete snippet
Deletes an existing snippet.
```
DELETE /snippets/:id
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | Integer | yes | The ID of a snippet |
```
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/snippets/1"
```
upon successful delete a `204 No content` HTTP code shall be expected, with no data,
but if the snippet is non-existent, a `404 Not Found` will be returned.
## Explore all public snippets
```
GET /snippets/public
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `per_page` | Integer | no | number of snippets to return per page |
| `page` | Integer | no | the page to retrieve |
``` bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/snippets/public?per_page=2&page=1
```
Example response:
``` json
[
{
"author": {
"avatar_url": "http://www.gravatar.com/avatar/edaf55a9e363ea263e3b981d09e0f7f7?s=80&d=identicon",
"id": 12,
"name": "Libby Rolfson",
"state": "active",
"username": "elton_wehner",
"web_url": "http://localhost:3000/elton_wehner"
},
"created_at": "2016-11-25T16:53:34.504Z",
"file_name": "oconnerrice.rb",
"id": 49,
"raw_url": "http://localhost:3000/snippets/49/raw",
"title": "Ratione cupiditate et laborum temporibus.",
"updated_at": "2016-11-25T16:53:34.504Z",
"web_url": "http://localhost:3000/snippets/49"
},
{
"author": {
"avatar_url": "http://www.gravatar.com/avatar/36583b28626de71061e6e5a77972c3bd?s=80&d=identicon",
"id": 16,
"name": "Llewellyn Flatley",
"state": "active",
"username": "adaline",
"web_url": "http://localhost:3000/adaline"
},
"created_at": "2016-11-25T16:53:34.479Z",
"file_name": "muellershields.rb",
"id": 48,
"raw_url": "http://localhost:3000/snippets/48/raw",
"title": "Minus similique nesciunt vel fugiat qui ullam sunt.",
"updated_at": "2016-11-25T16:53:34.479Z",
"web_url": "http://localhost:3000/snippets/48"
}
]
```
......@@ -33,7 +33,7 @@ built and deployed under a dynamic environment and can be previewed with an
also dynamically URL.
The details of the Review Apps implementation depend widely on your real
technology stack and on your deployment process. The simplest case it to
technology stack and on your deployment process. The simplest case is to
deploy a simple static HTML website, but it will not be that straightforward
when your app is using a database for example. To make a branch be deployed
on a temporary instance and booting up this instance with all required software
......
@admin
Feature: Admin Settings
Background:
Given I sign in as an admin
And I visit admin settings page
Scenario: Change application settings
When I modify settings and save form
Then I should see application settings saved
Scenario: Change Slack Service Template settings
When I click on "Service Templates"
And I click on "Slack" service
And I fill out Slack settings
Then I check all events and submit form
And I should see service template settings saved
Then I click on "Slack" service
And I should see all checkboxes checked
And I should see Slack settings saved
......@@ -22,7 +22,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
end
step 'I click link "New snippet"' do
click_link "New snippet"
first(:link, "New snippet").click
end
step 'I click link "Snippet one"' do
......
......@@ -203,10 +203,6 @@ module SharedPaths
visit admin_teams_path
end
step 'I visit admin settings page' do
visit admin_application_settings_path
end
step 'I visit spam logs page' do
visit admin_spam_logs_path
end
......
......@@ -64,6 +64,7 @@ module API
mount ::API::Session
mount ::API::Settings
mount ::API::SidekiqMetrics
mount ::API::Snippets
mount ::API::Subscriptions
mount ::API::SystemHooks
mount ::API::Tags
......
......@@ -23,9 +23,9 @@ module API
success Entities::RepoBranch
end
params do
requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
requires :branch, type: String, desc: 'The name of the branch'
end
get ':id/repository/branches/:branch' do
get ':id/repository/branches/:branch', requirements: { branch: /.+/ } do
branch = user_project.repository.find_branch(params[:branch])
not_found!("Branch") unless branch
......@@ -39,11 +39,11 @@ module API
success Entities::RepoBranch
end
params do
requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
requires :branch, type: String, desc: 'The name of the branch'
optional :developers_can_push, type: Boolean, desc: 'Flag if developers can push to that branch'
optional :developers_can_merge, type: Boolean, desc: 'Flag if developers can merge to that branch'
end
put ':id/repository/branches/:branch/protect' do
put ':id/repository/branches/:branch/protect', requirements: { branch: /.+/ } do
authorize_admin_project
branch = user_project.repository.find_branch(params[:branch])
......@@ -76,9 +76,9 @@ module API
success Entities::RepoBranch
end
params do
requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
requires :branch, type: String, desc: 'The name of the branch'
end
put ':id/repository/branches/:branch/unprotect' do
put ':id/repository/branches/:branch/unprotect', requirements: { branch: /.+/ } do
authorize_admin_project
branch = user_project.repository.find_branch(params[:branch])
......@@ -112,9 +112,9 @@ module API
desc 'Delete a branch'
params do
requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
requires :branch, type: String, desc: 'The name of the branch'
end
delete ":id/repository/branches/:branch" do
delete ":id/repository/branches/:branch", requirements: { branch: /.+/ } do
authorize_push_project
result = DeleteBranchService.new(user_project, current_user).
......
require 'mime/types'
module API
# Projects commits API
class Commits < Grape::API
include PaginationParams
......@@ -121,6 +120,41 @@ module API
present paginate(notes), with: Entities::CommitNote
end
desc 'Cherry pick commit into a branch' do
detail 'This feature was introduced in GitLab 8.15'
success Entities::RepoCommit
end
params do
requires :sha, type: String, desc: 'A commit sha to be cherry picked'
requires :branch, type: String, desc: 'The name of the branch'
end
post ':id/repository/commits/:sha/cherry_pick' do
authorize! :push_code, user_project
commit = user_project.commit(params[:sha])
not_found!('Commit') unless commit
branch = user_project.repository.find_branch(params[:branch])
not_found!('Branch') unless branch
commit_params = {
commit: commit,
create_merge_request: false,
source_project: user_project,
source_branch: commit.cherry_pick_branch_name,
target_branch: params[:branch]
}
result = ::Commits::CherryPickService.new(user_project, current_user, commit_params).execute
if result[:status] == :success
branch = user_project.repository.find_branch(params[:branch])
present user_project.repository.commit(branch.dereferenced_target), with: Entities::RepoCommit
else
render_api_error!(result[:message], 400)
end
end
desc 'Post comment to commit' do
success Entities::CommitNote
end
......
......@@ -201,6 +201,19 @@ module API
end
end
class PersonalSnippet < Grape::Entity
expose :id, :title, :file_name
expose :author, using: Entities::UserBasic
expose :updated_at, :created_at
expose :web_url do |snippet|
Gitlab::UrlBuilder.build(snippet)
end
expose :raw_url do |snippet|
Gitlab::UrlBuilder.build(snippet) + "/raw"
end
end
class ProjectEntity < Grape::Entity
expose :id, :iid
expose(:project_id) { |entity| entity.project.id }
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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