Commit 16808a27 authored by Valery Sizov's avatar Valery Sizov

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into orderable-issues

parents 6af8547a 82f6c0f5
...@@ -20,7 +20,7 @@ gem 'rugged', '~> 0.24.0' ...@@ -20,7 +20,7 @@ gem 'rugged', '~> 0.24.0'
# Authentication libraries # Authentication libraries
gem 'devise', '~> 4.2' gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0' gem 'doorkeeper', '~> 4.2.0'
gem 'omniauth', '~> 1.3.2' gem 'omniauth', '~> 1.4.2'
gem 'omniauth-auth0', '~> 1.4.1' gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6' gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-cas3', '~> 1.1.2' gem 'omniauth-cas3', '~> 1.1.2'
......
...@@ -328,7 +328,7 @@ GEM ...@@ -328,7 +328,7 @@ GEM
temple (~> 0.7.6) temple (~> 0.7.6)
thor thor
tilt tilt
hashie (3.4.4) hashie (3.5.5)
health_check (2.2.1) health_check (2.2.1)
rails (>= 4.0) rails (>= 4.0)
hipchat (1.5.2) hipchat (1.5.2)
...@@ -441,7 +441,7 @@ GEM ...@@ -441,7 +441,7 @@ GEM
octokit (4.6.2) octokit (4.6.2)
sawyer (~> 0.8.0, >= 0.5.3) sawyer (~> 0.8.0, >= 0.5.3)
oj (2.17.4) oj (2.17.4)
omniauth (1.3.2) omniauth (1.4.2)
hashie (>= 1.2, < 4) hashie (>= 1.2, < 4)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
omniauth-auth0 (1.4.1) omniauth-auth0 (1.4.1)
...@@ -920,7 +920,7 @@ DEPENDENCIES ...@@ -920,7 +920,7 @@ DEPENDENCIES
oauth2 (~> 1.2.0) oauth2 (~> 1.2.0)
octokit (~> 4.6.2) octokit (~> 4.6.2)
oj (~> 2.17.4) oj (~> 2.17.4)
omniauth (~> 1.3.2) omniauth (~> 1.4.2)
omniauth-auth0 (~> 1.4.1) omniauth-auth0 (~> 1.4.1)
omniauth-authentiq (~> 0.3.0) omniauth-authentiq (~> 0.3.0)
omniauth-azure-oauth2 (~> 0.0.6) omniauth-azure-oauth2 (~> 0.0.6)
......
...@@ -6,12 +6,8 @@ ...@@ -6,12 +6,8 @@
/* global AwardsHandler */ /* global AwardsHandler */
/* global Aside */ /* global Aside */
function requireAll(context) { return context.keys().map(context); }
window.$ = window.jQuery = require('jquery'); window.$ = window.jQuery = require('jquery');
require('jquery-ui/ui/autocomplete');
require('jquery-ui/ui/draggable'); require('jquery-ui/ui/draggable');
require('jquery-ui/ui/effect-highlight');
require('jquery-ui/ui/sortable'); require('jquery-ui/ui/sortable');
require('jquery-ujs'); require('jquery-ujs');
require('vendor/jquery.endless-scroll'); require('vendor/jquery.endless-scroll');
...@@ -46,15 +42,176 @@ require('./shortcuts_dashboard_navigation'); ...@@ -46,15 +42,176 @@ require('./shortcuts_dashboard_navigation');
require('./shortcuts_issuable'); require('./shortcuts_issuable');
require('./shortcuts_network'); require('./shortcuts_network');
require('vendor/jquery.nicescroll'); require('vendor/jquery.nicescroll');
requireAll(require.context('./behaviors', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./blob', false, /^\.\/.*\.(js|es6)$/)); // behaviors
requireAll(require.context('./templates', false, /^\.\/.*\.(js|es6)$/)); require('./behaviors/autosize');
requireAll(require.context('./commit', false, /^\.\/.*\.(js|es6)$/)); require('./behaviors/details_behavior');
requireAll(require.context('./extensions', false, /^\.\/.*\.(js|es6)$/)); require('./behaviors/quick_submit');
requireAll(require.context('./lib/utils', false, /^\.\/.*\.(js|es6)$/)); require('./behaviors/requires_input');
requireAll(require.context('./u2f', false, /^\.\/.*\.(js|es6)$/)); require('./behaviors/toggler_behavior');
requireAll(require.context('./droplab', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('.', false, /^\.\/(?!application\.js).*\.(js|es6)$/)); // blob
require('./blob/blob_ci_yaml');
require('./blob/blob_dockerfile_selector');
require('./blob/blob_dockerfile_selectors');
require('./blob/blob_file_dropzone');
require('./blob/blob_gitignore_selector');
require('./blob/blob_gitignore_selectors');
require('./blob/blob_license_selector');
require('./blob/blob_license_selectors');
require('./blob/template_selector');
// templates
require('./templates/issuable_template_selector');
require('./templates/issuable_template_selectors');
// commit
require('./commit/file.js');
require('./commit/image_file.js');
// extensions
require('./extensions/array');
require('./extensions/custom_event');
require('./extensions/element');
require('./extensions/jquery');
require('./extensions/object');
// lib/utils
require('./lib/utils/animate');
require('./lib/utils/bootstrap_linked_tabs');
require('./lib/utils/common_utils');
require('./lib/utils/datetime_utility');
require('./lib/utils/notify');
require('./lib/utils/pretty_time');
require('./lib/utils/text_utility');
require('./lib/utils/type_utility');
require('./lib/utils/url_utility');
// u2f
require('./u2f/authenticate');
require('./u2f/error');
require('./u2f/register');
require('./u2f/util');
// droplab
require('./droplab/droplab');
require('./droplab/droplab_ajax');
require('./droplab/droplab_ajax_filter');
require('./droplab/droplab_filter');
// everything else
require('./abuse_reports');
require('./activities');
require('./admin');
require('./ajax_loading_spinner');
require('./api');
require('./aside');
require('./autosave');
require('./awards_handler');
require('./breakpoints');
require('./broadcast_message');
require('./build');
require('./build_artifacts');
require('./build_variables');
require('./ci_lint_editor');
require('./commit');
require('./commits');
require('./compare');
require('./compare_autocomplete');
require('./confirm_danger_modal');
require('./copy_as_gfm');
require('./copy_to_clipboard');
require('./create_label');
require('./diff');
require('./dispatcher');
require('./dropzone_input');
require('./due_date_select');
require('./files_comment_button');
require('./flash');
require('./gfm_auto_complete');
require('./gl_dropdown');
require('./gl_field_error');
require('./gl_field_errors');
require('./gl_form');
require('./group_avatar');
require('./group_label_subscription');
require('./groups_select');
require('./header');
require('./importer_status');
require('./issuable');
require('./issuable_context');
require('./issuable_form');
require('./issue');
require('./issue_status_select');
require('./issues_bulk_assignment');
require('./label_manager');
require('./labels');
require('./labels_select');
require('./layout_nav');
require('./line_highlighter');
require('./logo');
require('./member_expiration_date');
require('./members');
require('./merge_request');
require('./merge_request_tabs');
require('./merge_request_widget');
require('./merged_buttons');
require('./milestone');
require('./milestone_select');
require('./mini_pipeline_graph_dropdown');
require('./namespace_select');
require('./new_branch_form');
require('./new_commit_form');
require('./notes');
require('./notifications_dropdown');
require('./notifications_form');
require('./pager');
require('./pipelines');
require('./preview_markdown');
require('./project');
require('./project_avatar');
require('./project_find_file');
require('./project_fork');
require('./project_import');
require('./project_label_subscription');
require('./project_new');
require('./project_select');
require('./project_show');
require('./project_variables');
require('./projects_list');
require('./render_gfm');
require('./render_math');
require('./right_sidebar');
require('./search');
require('./search_autocomplete');
require('./shortcuts');
require('./shortcuts_blob');
require('./shortcuts_dashboard_navigation');
require('./shortcuts_find_file');
require('./shortcuts_issuable');
require('./shortcuts_navigation');
require('./shortcuts_network');
require('./signin_tabs_memoizer');
require('./single_file_diff');
require('./smart_interval');
require('./snippets_list');
require('./star');
require('./subbable_resource');
require('./subscription');
require('./subscription_select');
require('./syntax_highlight');
require('./task_list');
require('./todos');
require('./tree');
require('./user');
require('./user_tabs');
require('./username_validator');
require('./users_select');
require('./version_check_image');
require('./visibility_select');
require('./wikis');
require('./zen_mode');
require('vendor/fuzzaldrin-plus'); require('vendor/fuzzaldrin-plus');
require('es6-promise').polyfill(); require('es6-promise').polyfill();
......
/* global Vue */
require('./issue_card_inner');
const Store = gl.issueBoards.BoardsStore;
module.exports = {
name: 'BoardsIssueCard',
template: `
<li class="card"
:class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }"
:index="index"
:data-issue-id="issue.id"
@mousedown="mouseDown"
@mousemove="mouseMove"
@mouseup="showIssue($event)">
<issue-card-inner
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:root-path="rootPath" />
</li>
`,
components: {
'issue-card-inner': gl.issueBoards.IssueCardInner,
},
props: {
list: Object,
issue: Object,
issueLinkBase: String,
disabled: Boolean,
index: Number,
rootPath: String,
},
data() {
return {
showDetail: false,
detailIssue: Store.detail,
};
},
computed: {
issueDetailVisible() {
return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
},
},
methods: {
mouseDown() {
this.showDetail = true;
},
mouseMove() {
this.showDetail = false;
},
showIssue(e) {
const targetTagName = e.target.tagName.toLowerCase();
if (targetTagName === 'a' || targetTagName === 'button') return;
if (this.showDetail) {
this.showDetail = false;
if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
Store.detail.issue = {};
} else {
Store.detail.issue = this.issue;
Store.detail.list = this.list;
}
}
},
},
};
/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */
/* global Vue */
require('./issue_card_inner');
(() => {
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardCard = Vue.extend({
template: '#js-board-list-card',
components: {
'issue-card-inner': gl.issueBoards.IssueCardInner,
},
props: {
list: Object,
issue: Object,
issueLinkBase: String,
disabled: Boolean,
index: Number,
rootPath: String,
},
data () {
return {
showDetail: false,
detailIssue: Store.detail
};
},
computed: {
issueDetailVisible () {
return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
}
},
methods: {
mouseDown () {
this.showDetail = true;
},
mouseMove() {
this.showDetail = false;
},
showIssue (e) {
const targetTagName = e.target.tagName.toLowerCase();
if (targetTagName === 'a' || targetTagName === 'button') return;
if (this.showDetail) {
this.showDetail = false;
if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
Store.detail.issue = {};
} else {
Store.detail.issue = this.issue;
Store.detail.list = this.list;
}
}
}
}
});
})();
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
/* global Vue */ /* global Vue */
/* global Sortable */ /* global Sortable */
require('./board_card'); const boardCard = require('./board_card');
require('./board_new_issue'); require('./board_new_issue');
(() => { (() => {
...@@ -14,7 +14,7 @@ require('./board_new_issue'); ...@@ -14,7 +14,7 @@ require('./board_new_issue');
gl.issueBoards.BoardList = Vue.extend({ gl.issueBoards.BoardList = Vue.extend({
template: '#js-board-list-template', template: '#js-board-list-template',
components: { components: {
'board-card': gl.issueBoards.BoardCard, boardCard,
'board-new-issue': gl.issueBoards.BoardNewIssue 'board-new-issue': gl.issueBoards.BoardNewIssue
}, },
props: { props: {
......
...@@ -134,19 +134,31 @@ class List { ...@@ -134,19 +134,31 @@ class List {
if (listFrom) { if (listFrom) {
this.issuesSize += 1; this.issuesSize += 1;
<<<<<<< HEAD
gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid) gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid)
.then(() => { .then(() => {
listFrom.getIssues(false); listFrom.getIssues(false);
}); });
=======
this.updateIssueLabel(issue, listFrom);
>>>>>>> 82f6c0f5ac4ed29390ed90592d2c431f3494d93f
} }
} }
} }
<<<<<<< HEAD
moveIssue (issue, oldIndex, newIndex, moveBeforeIid, moveAfterIid) { moveIssue (issue, oldIndex, newIndex, moveBeforeIid, moveAfterIid) {
this.issues.splice(oldIndex, 1); this.issues.splice(oldIndex, 1);
this.issues.splice(newIndex, 0, issue); this.issues.splice(newIndex, 0, issue);
gl.boardService.moveIssue(issue.id, null, null, moveBeforeIid, moveAfterIid); gl.boardService.moveIssue(issue.id, null, null, moveBeforeIid, moveAfterIid);
=======
updateIssueLabel(issue, listFrom) {
gl.boardService.moveIssue(issue.id, listFrom.id, this.id)
.then(() => {
listFrom.getIssues(false);
});
>>>>>>> 82f6c0f5ac4ed29390ed90592d2c431f3494d93f
} }
findIssue (id) { findIssue (id) {
......
...@@ -92,9 +92,12 @@ ...@@ -92,9 +92,12 @@
const issueLists = issue.getLists(); const issueLists = issue.getLists();
const listLabels = issueLists.map(listIssue => listIssue.label); const listLabels = issueLists.map(listIssue => listIssue.label);
// Add to new lists issues if it doesn't already exist
if (!issueTo) { if (!issueTo) {
// Add to new lists issues if it doesn't already exist
listTo.addIssue(issue, listFrom, newIndex); listTo.addIssue(issue, listFrom, newIndex);
} else {
listTo.updateIssueLabel(issue, listFrom);
issueTo.removeLabel(listFrom.label);
} }
if (listTo.type === 'done') { if (listTo.type === 'done') {
......
...@@ -36,6 +36,7 @@ ...@@ -36,6 +36,7 @@
/* global Shortcuts */ /* global Shortcuts */
const ShortcutsBlob = require('./shortcuts_blob'); const ShortcutsBlob = require('./shortcuts_blob');
const UserCallout = require('./user_callout');
(function() { (function() {
var Dispatcher; var Dispatcher;
...@@ -277,6 +278,9 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -277,6 +278,9 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'ci:lints:show': case 'ci:lints:show':
new gl.CILintEditor(); new gl.CILintEditor();
break; break;
case 'users:show':
new UserCallout();
break;
} }
switch (path.first()) { switch (path.first()) {
case 'sessions': case 'sessions':
...@@ -313,6 +317,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -313,6 +317,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'dashboard': case 'dashboard':
case 'root': case 'root':
shortcut_handler = new ShortcutsDashboardNavigation(); shortcut_handler = new ShortcutsDashboardNavigation();
new UserCallout();
break; break;
case 'profiles': case 'profiles':
new NotificationsForm(); new NotificationsForm();
......
require('./stat_graph_contributors_graph'); import ContributorsStatGraph from './stat_graph_contributors';
require('./stat_graph_contributors_util');
require('./stat_graph_contributors'); // export to global scope
require('./stat_graph'); window.ContributorsStatGraph = ContributorsStatGraph;
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-return-assign, max-len */
(function() {
this.StatGraph = (function() {
function StatGraph() {}
StatGraph.log = {};
StatGraph.get_log = function() {
return this.log;
};
StatGraph.set_log = function(data) {
return this.log = data;
};
return StatGraph;
})();
}).call(window);
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, no-shadow */
/* global ContributorsGraph */
/* global ContributorsAuthorGraph */
/* global ContributorsMasterGraph */
/* global ContributorsStatGraphUtil */
/* global d3 */
window.d3 = require('d3'); import d3 from 'd3';
import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph';
import ContributorsStatGraphUtil from './stat_graph_contributors_util';
(function() { export default (function() {
this.ContributorsStatGraph = (function() { function ContributorsStatGraph() {}
function ContributorsStatGraph() {}
ContributorsStatGraph.prototype.init = function(log) { ContributorsStatGraph.prototype.init = function(log) {
var author_commits, total_commits; var author_commits, total_commits;
this.parsed_log = ContributorsStatGraphUtil.parse_log(log); this.parsed_log = ContributorsStatGraphUtil.parse_log(log);
this.set_current_field("commits"); this.set_current_field("commits");
total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field); author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field);
this.add_master_graph(total_commits); this.add_master_graph(total_commits);
this.add_authors_graph(author_commits); this.add_authors_graph(author_commits);
return this.change_date_header(); return this.change_date_header();
}; };
ContributorsStatGraph.prototype.add_master_graph = function(total_data) { ContributorsStatGraph.prototype.add_master_graph = function(total_data) {
this.master_graph = new ContributorsMasterGraph(total_data); this.master_graph = new ContributorsMasterGraph(total_data);
return this.master_graph.draw(); return this.master_graph.draw();
}; };
ContributorsStatGraph.prototype.add_authors_graph = function(author_data) { ContributorsStatGraph.prototype.add_authors_graph = function(author_data) {
var limited_author_data; var limited_author_data;
this.authors = []; this.authors = [];
limited_author_data = author_data.slice(0, 100); limited_author_data = author_data.slice(0, 100);
return _.each(limited_author_data, (function(_this) { return _.each(limited_author_data, (function(_this) {
return function(d) { return function(d) {
var author_graph, author_header; var author_graph, author_header;
author_header = _this.create_author_header(d); author_header = _this.create_author_header(d);
$(".contributors-list").append(author_header); $(".contributors-list").append(author_header);
_this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates); _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates);
return author_graph.draw(); return author_graph.draw();
}; };
})(this)); })(this));
}; };
ContributorsStatGraph.prototype.format_author_commit_info = function(author) { ContributorsStatGraph.prototype.format_author_commit_info = function(author) {
var commits; var commits;
commits = $('<span/>', { commits = $('<span/>', {
"class": 'graph-author-commits-count' "class": 'graph-author-commits-count'
}); });
commits.text(author.commits + " commits"); commits.text(author.commits + " commits");
return $('<span/>').append(commits); return $('<span/>').append(commits);
}; };
ContributorsStatGraph.prototype.create_author_header = function(author) { ContributorsStatGraph.prototype.create_author_header = function(author) {
var author_commit_info, author_commit_info_span, author_email, author_name, list_item; var author_commit_info, author_commit_info_span, author_email, author_name, list_item;
list_item = $('<li/>', { list_item = $('<li/>', {
"class": 'person', "class": 'person',
style: 'display: block;' style: 'display: block;'
}); });
author_name = $('<h4>' + author.author_name + '</h4>'); author_name = $('<h4>' + author.author_name + '</h4>');
author_email = $('<p class="graph-author-email">' + author.author_email + '</p>'); author_email = $('<p class="graph-author-email">' + author.author_email + '</p>');
author_commit_info_span = $('<span/>', { author_commit_info_span = $('<span/>', {
"class": 'commits' "class": 'commits'
}); });
author_commit_info = this.format_author_commit_info(author); author_commit_info = this.format_author_commit_info(author);
author_commit_info_span.html(author_commit_info); author_commit_info_span.html(author_commit_info);
list_item.append(author_name); list_item.append(author_name);
list_item.append(author_email); list_item.append(author_email);
list_item.append(author_commit_info_span); list_item.append(author_commit_info_span);
return list_item; return list_item;
}; };
ContributorsStatGraph.prototype.redraw_master = function() { ContributorsStatGraph.prototype.redraw_master = function() {
var total_data; var total_data;
total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field); total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
this.master_graph.set_data(total_data); this.master_graph.set_data(total_data);
return this.master_graph.redraw(); return this.master_graph.redraw();
}; };
ContributorsStatGraph.prototype.redraw_authors = function() { ContributorsStatGraph.prototype.redraw_authors = function() {
var author_commits, x_domain; var author_commits, x_domain;
$("ol").html(""); $("ol").html("");
x_domain = ContributorsGraph.prototype.x_domain; x_domain = ContributorsGraph.prototype.x_domain;
author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain); author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain);
return _.each(author_commits, (function(_this) { return _.each(author_commits, (function(_this) {
return function(d) { return function(d) {
_this.redraw_author_commit_info(d); _this.redraw_author_commit_info(d);
$(_this.authors[d.author_name].list_item).appendTo("ol"); $(_this.authors[d.author_name].list_item).appendTo("ol");
_this.authors[d.author_name].set_data(d.dates); _this.authors[d.author_name].set_data(d.dates);
return _this.authors[d.author_name].redraw(); return _this.authors[d.author_name].redraw();
}; };
})(this)); })(this));
}; };
ContributorsStatGraph.prototype.set_current_field = function(field) { ContributorsStatGraph.prototype.set_current_field = function(field) {
return this.field = field; return this.field = field;
}; };
ContributorsStatGraph.prototype.change_date_header = function() { ContributorsStatGraph.prototype.change_date_header = function() {
var print, print_date_format, x_domain; var print, print_date_format, x_domain;
x_domain = ContributorsGraph.prototype.x_domain; x_domain = ContributorsGraph.prototype.x_domain;
print_date_format = d3.time.format("%B %e %Y"); print_date_format = d3.time.format("%B %e %Y");
print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]); print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]);
return $("#date_header").text(print); return $("#date_header").text(print);
}; };
ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) { ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) {
var author_commit_info, author_list_item; var author_commit_info, author_list_item;
author_list_item = $(this.authors[author.author_name].list_item); author_list_item = $(this.authors[author.author_name].list_item);
author_commit_info = this.format_author_commit_info(author); author_commit_info = this.format_author_commit_info(author);
return author_list_item.find("span").html(author_commit_info); return author_list_item.find("span").html(author_commit_info);
}; };
return ContributorsStatGraph; return ContributorsStatGraph;
})(); })();
}).call(window);
/* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */ /* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */
(function() {
window.ContributorsStatGraphUtil = { export default {
parse_log: function(log) { parse_log: function(log) {
var by_author, by_email, data, entry, i, len, total, normalized_email; var by_author, by_email, data, entry, i, len, total, normalized_email;
total = {}; total = {};
by_author = {}; by_author = {};
by_email = {}; by_email = {};
for (i = 0, len = log.length; i < len; i += 1) { for (i = 0, len = log.length; i < len; i += 1) {
entry = log[i]; entry = log[i];
if (total[entry.date] == null) { if (total[entry.date] == null) {
this.add_date(entry.date, total); this.add_date(entry.date, total);
}
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);
}
if (!data[entry.date]) {
this.add_date(entry.date, data);
}
this.store_data(entry, total[entry.date], data[entry.date]);
}
total = _.toArray(total);
by_author = _.toArray(by_author);
return {
total: total,
by_author: by_author
};
},
add_date: function(date, collection) {
collection[date] = {};
return collection[date].date = date;
},
add_author: function(author, by_author, by_email) {
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;
by_email[normalized_email] = data;
return data;
},
store_data: function(entry, total, by_author) {
this.store_commits(total, by_author);
this.store_additions(entry, total, by_author);
return this.store_deletions(entry, total, by_author);
},
store_commits: function(total, by_author) {
this.add(total, "commits", 1);
return this.add(by_author, "commits", 1);
},
add: function(collection, field, value) {
if (collection[field] == null) {
collection[field] = 0;
}
return collection[field] += value;
},
store_additions: function(entry, total, by_author) {
if (entry.additions == null) {
entry.additions = 0;
} }
this.add(total, "additions", entry.additions); normalized_email = entry.author_email.toLowerCase();
return this.add(by_author, "additions", entry.additions); data = by_author[entry.author_name] || by_email[normalized_email];
}, if (data == null) {
store_deletions: function(entry, total, by_author) { data = this.add_author(entry, by_author, by_email);
if (entry.deletions == null) {
entry.deletions = 0;
} }
this.add(total, "deletions", entry.deletions); if (!data[entry.date]) {
return this.add(by_author, "deletions", entry.deletions); this.add_date(entry.date, data);
},
get_total_data: function(parsed_log, field) {
var log, total_data;
log = parsed_log.total;
total_data = this.pick_field(log, field);
return _.sortBy(total_data, function(d) {
return d.date;
});
},
pick_field: function(log, field) {
var total_data;
total_data = [];
_.each(log, function(d) {
return total_data.push(_.pick(d, [field, 'date']));
});
return total_data;
},
get_author_data: function(parsed_log, field, date_range) {
var author_data, log;
if (date_range == null) {
date_range = null;
}
log = parsed_log.by_author;
author_data = [];
_.each(log, (function(_this) {
return function(log_entry) {
var parsed_log_entry;
parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range);
if (!_.isEmpty(parsed_log_entry.dates)) {
return author_data.push(parsed_log_entry);
}
};
})(this));
return _.sortBy(author_data, function(d) {
return d[field];
}).reverse();
},
parse_log_entry: function(log_entry, field, date_range) {
var parsed_entry;
parsed_entry = {};
parsed_entry.author_name = log_entry.author_name;
parsed_entry.author_email = log_entry.author_email;
parsed_entry.dates = {};
parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0;
_.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) {
return function(value, key) {
if (_this.in_range(value.date, date_range)) {
parsed_entry.dates[value.date] = value[field];
parsed_entry.commits += value.commits;
parsed_entry.additions += value.additions;
return parsed_entry.deletions += value.deletions;
}
};
})(this));
return parsed_entry;
},
in_range: function(date, date_range) {
var ref;
if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) {
return true;
} else {
return false;
} }
this.store_data(entry, total[entry.date], data[entry.date]);
}
total = _.toArray(total);
by_author = _.toArray(by_author);
return {
total: total,
by_author: by_author
};
},
add_date: function(date, collection) {
collection[date] = {};
return collection[date].date = date;
},
add_author: function(author, by_author, by_email) {
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;
by_email[normalized_email] = data;
return data;
},
store_data: function(entry, total, by_author) {
this.store_commits(total, by_author);
this.store_additions(entry, total, by_author);
return this.store_deletions(entry, total, by_author);
},
store_commits: function(total, by_author) {
this.add(total, "commits", 1);
return this.add(by_author, "commits", 1);
},
add: function(collection, field, value) {
if (collection[field] == null) {
collection[field] = 0;
}
return collection[field] += value;
},
store_additions: function(entry, total, by_author) {
if (entry.additions == null) {
entry.additions = 0;
}
this.add(total, "additions", entry.additions);
return this.add(by_author, "additions", entry.additions);
},
store_deletions: function(entry, total, by_author) {
if (entry.deletions == null) {
entry.deletions = 0;
}
this.add(total, "deletions", entry.deletions);
return this.add(by_author, "deletions", entry.deletions);
},
get_total_data: function(parsed_log, field) {
var log, total_data;
log = parsed_log.total;
total_data = this.pick_field(log, field);
return _.sortBy(total_data, function(d) {
return d.date;
});
},
pick_field: function(log, field) {
var total_data;
total_data = [];
_.each(log, function(d) {
return total_data.push(_.pick(d, [field, 'date']));
});
return total_data;
},
get_author_data: function(parsed_log, field, date_range) {
var author_data, log;
if (date_range == null) {
date_range = null;
}
log = parsed_log.by_author;
author_data = [];
_.each(log, (function(_this) {
return function(log_entry) {
var parsed_log_entry;
parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range);
if (!_.isEmpty(parsed_log_entry.dates)) {
return author_data.push(parsed_log_entry);
}
};
})(this));
return _.sortBy(author_data, function(d) {
return d[field];
}).reverse();
},
parse_log_entry: function(log_entry, field, date_range) {
var parsed_entry;
parsed_entry = {};
parsed_entry.author_name = log_entry.author_name;
parsed_entry.author_email = log_entry.author_email;
parsed_entry.dates = {};
parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0;
_.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) {
return function(value, key) {
if (_this.in_range(value.date, date_range)) {
parsed_entry.dates[value.date] = value[field];
parsed_entry.commits += value.commits;
parsed_entry.additions += value.additions;
return parsed_entry.deletions += value.deletions;
}
};
})(this));
return parsed_entry;
},
in_range: function(date, date_range) {
var ref;
if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) {
return true;
} else {
return false;
} }
}; }
}).call(window); };
...@@ -296,5 +296,57 @@ ...@@ -296,5 +296,57 @@
* @returns {Boolean} * @returns {Boolean}
*/ */
w.gl.utils.convertPermissionToBoolean = permission => permission === 'true'; w.gl.utils.convertPermissionToBoolean = permission => permission === 'true';
/**
* Back Off exponential algorithm
* backOff :: (Function<next, stop>, Number) -> Promise<Any, Error>
*
* @param {Function<next, stop>} fn function to be called
* @param {Number} timeout
* @return {Promise<Any, Error>}
* @example
* ```
* backOff(function (next, stop) {
* // Let's perform this function repeatedly for 60s or for the timeout provided.
*
* ourFunction()
* .then(function (result) {
* // continue if result is not what we need
* next();
*
* // when result is what we need let's stop with the repetions and jump out of the cycle
* stop(result);
* })
* .catch(function (error) {
* // if there is an error, we need to stop this with an error.
* stop(error);
* })
* }, 60000)
* .then(function (result) {})
* .catch(function (error) {
* // deal with errors passed to stop()
* })
* ```
*/
w.gl.utils.backOff = (fn, timeout = 60000) => {
let nextInterval = 2000;
const startTime = (+new Date());
return new Promise((resolve, reject) => {
const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg));
const next = () => {
if (new Date().getTime() - startTime < timeout) {
setTimeout(fn.bind(null, next, stop), nextInterval);
nextInterval *= 2;
} else {
reject(new Error('BACKOFF_TIMEOUT'));
}
};
fn(next, stop);
});
};
})(window); })(window);
}).call(window); }).call(window);
...@@ -78,7 +78,6 @@ ...@@ -78,7 +78,6 @@
} else { } else {
$(element).find('.assignee-icon').empty(); $(element).find('.assignee-icon').empty();
} }
return $(element).effect('highlight');
}; };
function Milestone() { function Milestone() {
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
$value = $block.find('.value'); $value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut(); $loading = $block.find('.block-loading').fadeOut();
if (issueUpdateURL) { if (issueUpdateURL) {
milestoneLinkTemplate = _.template('<a href="/<%- namespace %>/<%- path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>'); milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
milestoneLinkNoneTemplate = '<span class="no-value">None</span>'; milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>'); collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>');
} }
...@@ -181,8 +181,7 @@ ...@@ -181,8 +181,7 @@
$selectbox.hide(); $selectbox.hide();
$value.css('display', ''); $value.css('display', '');
if (data.milestone != null) { if (data.milestone != null) {
data.milestone.namespace = _this.currentProject.namespace; data.milestone.full_path = _this.currentProject.full_path;
data.milestone.path = _this.currentProject.path;
data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date); data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date);
$value.html(milestoneLinkTemplate(data.milestone)); $value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone)); return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
......
/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len, object-shorthand */
(function() { (function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; }; indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; };
...@@ -20,15 +20,35 @@ ...@@ -20,15 +20,35 @@
}; };
NewBranchForm.prototype.init = function() { NewBranchForm.prototype.init = function() {
if (this.name.val().length > 0) { if (this.name.length && this.name.val().length > 0) {
return this.name.trigger('blur'); return this.name.trigger('blur');
} }
}; };
NewBranchForm.prototype.setupAvailableRefs = function(availableRefs) { NewBranchForm.prototype.setupAvailableRefs = function(availableRefs) {
return this.ref.autocomplete({ var $branchSelect = $('.js-branch-select');
source: availableRefs,
minLength: 1 $branchSelect.glDropdown({
data: availableRefs,
filterable: true,
filterByText: true,
remote: false,
fieldName: $branchSelect.data('field-name'),
selectable: true,
isSelectable: function(branch, $el) {
return !$el.hasClass('is-active');
},
text: function(branch) {
return branch;
},
id: function(branch) {
return branch;
},
toggleLabel: function(branch) {
if (branch) {
return branch;
}
}
}); });
}; };
......
// require everything else in this directory require('./gl_crop');
function requireAll(context) { return context.keys().map(context); } require('./profile');
requireAll(require.context('.', false, /^\.\/(?!profile_bundle).*\.(js|es6)$/));
...@@ -36,6 +36,9 @@ ...@@ -36,6 +36,9 @@
// Do not update if one dropdown has not selected any option // Do not update if one dropdown has not selected any option
if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
this.$allowedToMergeDropdown.disable();
this.$allowedToPushDropdown.disable();
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: this.$wrap.data('url'), url: this.$wrap.data('url'),
...@@ -53,13 +56,13 @@ ...@@ -53,13 +56,13 @@
}] }]
} }
}, },
success: () => {
this.$wrap.effect('highlight');
},
error() { error() {
$.scrollTo(0); $.scrollTo(0);
new Flash('Failed to update branch!'); new Flash('Failed to update branch!');
} }
}).always(() => {
this.$allowedToMergeDropdown.enable();
this.$allowedToPushDropdown.enable();
}); });
} }
}; };
......
// require everything else in this directory require('./protected_branch_access_dropdown');
function requireAll(context) { return context.keys().map(context); } require('./protected_branch_create');
requireAll(require.context('.', false, /^\.\/(?!protected_branches_bundle).*\.(js|es6)$/)); require('./protected_branch_dropdown');
require('./protected_branch_edit');
require('./protected_branch_edit_list');
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */
/* global ace */ /* global ace */
// require everything else in this directory
function requireAll(context) { return context.keys().map(context); }
requireAll(require.context('.', false, /^\.\/(?!snippet_bundle).*\.(js|es6)$/));
(function() { (function() {
$(function() { $(function() {
var editor = ace.edit("editor"); var editor = ace.edit("editor");
......
/* global Cookies */
const userCalloutElementName = '.user-callout';
const closeButton = '.close-user-callout';
const userCalloutBtn = '.user-callout-btn';
const userCalloutSvgAttrName = 'callout-svg';
const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
const USER_CALLOUT_TEMPLATE = `
<div class="bordered-box landing content-block">
<button class="btn btn-default close close-user-callout" type="button">
<i class="fa fa-times dismiss-icon"></i>
</button>
<div class="row">
<div class="col-sm-3 col-xs-12 svg-container">
</div>
<div class="col-sm-8 col-xs-12 inner-content">
<h4>
Customize your experience
</h4>
<p>
Change syntax themes, default project pages, and more in preferences.
</p>
<a class="btn user-callout-btn" href="/profile/preferences">Check it out</a>
</div>
</div>
</div>`;
class UserCallout {
constructor() {
this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE);
this.userCalloutBody = $(userCalloutElementName);
this.userCalloutSvg = $(userCalloutElementName).attr(userCalloutSvgAttrName);
$(userCalloutElementName).removeAttr(userCalloutSvgAttrName);
this.init();
}
init() {
const $template = $(USER_CALLOUT_TEMPLATE);
if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') {
$template.find('.svg-container').append(this.userCalloutSvg);
this.userCalloutBody.append($template);
$template.find(closeButton).on('click', e => this.dismissCallout(e));
$template.find(userCalloutBtn).on('click', e => this.dismissCallout(e));
}
}
dismissCallout(e) {
Cookies.set(USER_CALLOUT_COOKIE, 'true');
const $currentTarget = $(e.currentTarget);
if ($currentTarget.hasClass('close-user-callout')) {
this.userCalloutBody.empty();
}
}
}
module.exports = UserCallout;
// require everything else in this directory require('./calendar');
function requireAll(context) { return context.keys().map(context); }
requireAll(require.context('.', false, /^\.\/(?!users_bundle).*\.(js|es6)$/));
/* global Vue, Flash, gl */ /* global Vue, Flash, gl */
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign, no-alert */
((gl) => { ((gl) => {
gl.VuePipelineActions = Vue.extend({ gl.VuePipelineActions = Vue.extend({
...@@ -16,6 +16,20 @@ ...@@ -16,6 +16,20 @@
download(name) { download(name) {
return `Download ${name} artifacts`; return `Download ${name} artifacts`;
}, },
/**
* Shows a dialog when the user clicks in the cancel button.
* We need to prevent the default behavior and stop propagation because the
* link relies on UJS.
*
* @param {Event} event
*/
confirmAction(event) {
if (!confirm('Are you sure you want to cancel this pipeline?')) {
event.preventDefault();
event.stopPropagation();
}
},
}, },
template: ` template: `
<td class="pipeline-actions hidden-xs"> <td class="pipeline-actions hidden-xs">
...@@ -87,6 +101,7 @@ ...@@ -87,6 +101,7 @@
</a> </a>
<a <a
v-if='pipeline.flags.cancelable' v-if='pipeline.flags.cancelable'
@click="confirmAction"
class="btn btn-remove has-tooltip" class="btn btn-remove has-tooltip"
title="Cancel" title="Cancel"
rel="nofollow" rel="nofollow"
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
* This is a manifest file that'll automatically include all the stylesheets available in this directory * This is a manifest file that'll automatically include all the stylesheets available in this directory
* and any sub-directories. You're free to add application-wide styles to this file and they'll appear at * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
* the top of the compiled file, but it's generally better to create a new file per style scope. * the top of the compiled file, but it's generally better to create a new file per style scope.
*= require jquery-ui/autocomplete
*= require jquery.atwho *= require jquery.atwho
*= require select2 *= require select2
*= require_self *= require_self
......
...@@ -2,17 +2,6 @@ ...@@ -2,17 +2,6 @@
font-family: $regular_font; font-family: $regular_font;
font-size: $font-size-base; font-size: $font-size-base;
&.ui-autocomplete {
border-color: $jq-ui-border;
padding: 0;
margin-top: 2px;
z-index: 1001;
.ui-menu-item a {
padding: 4px 10px;
}
}
.ui-state-default { .ui-state-default {
border: 1px solid $white-light; border: 1px solid $white-light;
background: $white-light; background: $white-light;
......
...@@ -277,3 +277,41 @@ table.u2f-registrations { ...@@ -277,3 +277,41 @@ table.u2f-registrations {
padding-left: 18px; padding-left: 18px;
} }
} }
.user-callout {
margin: 24px auto 0;
.bordered-box {
border: 1px solid $border-color;
border-radius: $border-radius-default;
}
.landing {
margin-bottom: $gl-padding;
.close {
margin-right: 20px;
}
.dismiss-icon {
float: right;
cursor: pointer;
color: $cycle-analytics-dismiss-icon-color;
}
.svg-container {
text-align: center;
svg {
width: 136px;
height: 136px;
}
}
}
@media(max-width: $screen-xs-max) {
.inner-content {
padding-left: 30px;
}
}
}
...@@ -52,7 +52,7 @@ module IssuablesHelper ...@@ -52,7 +52,7 @@ module IssuablesHelper
field_name: 'issuable_template', field_name: 'issuable_template',
selected: selected_template(issuable), selected: selected_template(issuable),
project_path: ref_project.path, project_path: ref_project.path,
namespace_path: ref_project.namespace.path namespace_path: ref_project.namespace.full_path
} }
} }
......
class Uniquify
# Return a version of the given 'base' string that is unique
# by appending a counter to it. Uniqueness is determined by
# repeated calls to the passed block.
#
# If `base` is a function/proc, we expect that calling it with a
# candidate counter returns a string to test/return.
def string(base)
@base = base
@counter = nil
increment_counter! while yield(base_string)
base_string
end
private
def base_string
if @base.respond_to?(:call)
@base.call(@counter)
else
"#{@base}#{@counter}"
end
end
def increment_counter!
@counter ||= 0
@counter += 1
end
end
...@@ -36,7 +36,7 @@ class Event < ActiveRecord::Base ...@@ -36,7 +36,7 @@ class Event < ActiveRecord::Base
scope :code_push, -> { where(action: PUSHED) } scope :code_push, -> { where(action: PUSHED) }
scope :in_projects, ->(projects) do scope :in_projects, ->(projects) do
where(project_id: projects).recent where(project_id: projects.pluck(:id)).recent
end end
scope :with_associations, -> { includes(:author, :project, project: :namespace).preload(:target) } scope :with_associations, -> { includes(:author, :project, project: :namespace).preload(:target) }
......
...@@ -98,14 +98,8 @@ class Namespace < ActiveRecord::Base ...@@ -98,14 +98,8 @@ class Namespace < ActiveRecord::Base
# Work around that by setting their username to "blank", followed by a counter. # Work around that by setting their username to "blank", followed by a counter.
path = "blank" if path.blank? path = "blank" if path.blank?
counter = 0 uniquify = Uniquify.new
base = path uniquify.string(path) { |s| Namespace.find_by_path_or_name(s) }
while Namespace.find_by_path_or_name(path)
counter += 1
path = "#{base}#{counter}"
end
path
end end
end end
......
...@@ -359,7 +359,7 @@ class Project < ActiveRecord::Base ...@@ -359,7 +359,7 @@ class Project < ActiveRecord::Base
end end
def reference_pattern def reference_pattern
name_pattern = Gitlab::Regex::NAMESPACE_REGEX_STR name_pattern = Gitlab::Regex::FULL_NAMESPACE_REGEX_STR
%r{ %r{
((?<namespace>#{name_pattern})\/)? ((?<namespace>#{name_pattern})\/)?
...@@ -847,10 +847,6 @@ class Project < ActiveRecord::Base ...@@ -847,10 +847,6 @@ class Project < ActiveRecord::Base
gitlab_shell.url_to_repo(path_with_namespace) gitlab_shell.url_to_repo(path_with_namespace)
end end
def namespace_dir
namespace.try(:path) || ''
end
def repo_exists? def repo_exists?
@repo_exists ||= repository.exists? @repo_exists ||= repository.exists?
rescue rescue
...@@ -899,8 +895,8 @@ class Project < ActiveRecord::Base ...@@ -899,8 +895,8 @@ class Project < ActiveRecord::Base
def rename_repo def rename_repo
path_was = previous_changes['path'].first path_was = previous_changes['path'].first
old_path_with_namespace = File.join(namespace_dir, path_was) old_path_with_namespace = File.join(namespace.full_path, path_was)
new_path_with_namespace = File.join(namespace_dir, path) new_path_with_namespace = File.join(namespace.full_path, path)
Rails.logger.error "Attempting to rename #{old_path_with_namespace} -> #{new_path_with_namespace}" Rails.logger.error "Attempting to rename #{old_path_with_namespace} -> #{new_path_with_namespace}"
......
...@@ -15,10 +15,10 @@ class MattermostService < ChatNotificationService ...@@ -15,10 +15,10 @@ class MattermostService < ChatNotificationService
'This service sends notifications about projects events to Mattermost channels.<br /> 'This service sends notifications about projects events to Mattermost channels.<br />
To set up this service: To set up this service:
<ol> <ol>
<li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation. </li> <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation.</li>
<li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event. </li> <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event.</li>
<li>Paste the webhook <strong>URL</strong> into the field bellow. </li> <li>Paste the webhook <strong>URL</strong> into the field below.</li>
<li>Select events below to enable notifications. The channel and username are optional. </li> <li>Select events below to enable notifications. The <strong>Channel handle</strong> and <strong>Username</strong> fields are optional.</li>
</ol>' </ol>'
end end
...@@ -28,14 +28,14 @@ class MattermostService < ChatNotificationService ...@@ -28,14 +28,14 @@ class MattermostService < ChatNotificationService
def default_fields def default_fields
[ [
{ type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' }, { type: 'text', name: 'webhook', placeholder: 'e.g. http://mattermost_host/hooks/…' },
{ type: 'text', name: 'username', placeholder: 'username' }, { type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_builds' }, { type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' }, { type: 'checkbox', name: 'notify_only_broken_pipelines' },
] ]
end end
def default_channel_placeholder def default_channel_placeholder
"town-square" "Channel handle (e.g. town-square)"
end end
end end
...@@ -13,11 +13,11 @@ class SlackService < ChatNotificationService ...@@ -13,11 +13,11 @@ class SlackService < ChatNotificationService
def help def help
'This service sends notifications about projects events to Slack channels.<br /> 'This service sends notifications about projects events to Slack channels.<br />
To setup this service: To set up this service:
<ol> <ol>
<li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event. </li> <li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event.</li>
<li>Paste the <strong>Webhook URL</strong> into the field below. </li> <li>Paste the <strong>Webhook URL</strong> into the field below.</li>
<li>Select events below to enable notifications. The channel and username are optional. </li> <li>Select events below to enable notifications. The <strong>Channel name</strong> and <strong>Username</strong> fields are optional.</li>
</ol>' </ol>'
end end
...@@ -27,14 +27,14 @@ class SlackService < ChatNotificationService ...@@ -27,14 +27,14 @@ class SlackService < ChatNotificationService
def default_fields def default_fields
[ [
{ type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, { type: 'text', name: 'webhook', placeholder: 'e.g. https://hooks.slack.com/services/…' },
{ type: 'text', name: 'username', placeholder: 'username' }, { type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_builds' }, { type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' }, { type: 'checkbox', name: 'notify_only_broken_pipelines' },
] ]
end end
def default_channel_placeholder def default_channel_placeholder
"#general" "Channel name (e.g. general)"
end end
end end
...@@ -109,9 +109,7 @@ class Repository ...@@ -109,9 +109,7 @@ class Repository
offset: offset, offset: offset,
after: after, after: after,
before: before, before: before,
# --follow doesn't play well with --skip. See: follow: path.present?,
# https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
follow: false,
skip_merges: skip_merges skip_merges: skip_merges
} }
...@@ -746,136 +744,63 @@ class Repository ...@@ -746,136 +744,63 @@ class Repository
@tags ||= raw_repository.tags @tags ||= raw_repository.tags
end end
# rubocop:disable Metrics/ParameterLists def create_dir(user, path, **options)
def commit_dir( options[:user] = user
user, path, options[:actions] = [{ action: :create_dir, file_path: path }]
message:, branch_name:,
author_email: nil, author_name: nil,
start_branch_name: nil, start_project: project)
check_tree_entry_for_dir(branch_name, path)
if start_branch_name
start_project.repository.
check_tree_entry_for_dir(start_branch_name, path)
end
commit_file( multi_action(**options)
user,
"#{path}/.gitkeep",
'',
message: message,
branch_name: branch_name,
update: false,
author_email: author_email,
author_name: author_name,
start_branch_name: start_branch_name,
start_project: start_project)
end end
# rubocop:enable Metrics/ParameterLists
# rubocop:disable Metrics/ParameterLists def create_file(user, path, content, **options)
def commit_file( options[:user] = user
user, path, content, options[:actions] = [{ action: :create, file_path: path, content: content }]
message:, branch_name:, update: true,
author_email: nil, author_name: nil,
start_branch_name: nil, start_project: project)
unless update
error_message = "Filename already exists; update not allowed"
if tree_entry_at(branch_name, path) multi_action(**options)
raise Gitlab::Git::Repository::InvalidBlobName.new(error_message) end
end
if start_branch_name && def update_file(user, path, content, **options)
start_project.repository.tree_entry_at(start_branch_name, path) previous_path = options.delete(:previous_path)
raise Gitlab::Git::Repository::InvalidBlobName.new(error_message) action = previous_path && previous_path != path ? :move : :update
end
end
multi_action( options[:user] = user
user: user, options[:actions] = [{ action: action, file_path: path, previous_path: previous_path, content: content }]
message: message,
branch_name: branch_name,
author_email: author_email,
author_name: author_name,
start_branch_name: start_branch_name,
start_project: start_project,
actions: [{ action: :create,
file_path: path,
content: content }])
end
# rubocop:enable Metrics/ParameterLists
# rubocop:disable Metrics/ParameterLists multi_action(**options)
def update_file(
user, path, content,
message:, branch_name:, previous_path:,
author_email: nil, author_name: nil,
start_branch_name: nil, start_project: project)
action = if previous_path && previous_path != path
:move
else
:update
end
multi_action(
user: user,
message: message,
branch_name: branch_name,
author_email: author_email,
author_name: author_name,
start_branch_name: start_branch_name,
start_project: start_project,
actions: [{ action: action,
file_path: path,
content: content,
previous_path: previous_path }])
end end
# rubocop:enable Metrics/ParameterLists
# rubocop:disable Metrics/ParameterLists def delete_file(user, path, **options)
def remove_file( options[:user] = user
user, path, options[:actions] = [{ action: :delete, file_path: path }]
message:, branch_name:,
author_email: nil, author_name: nil, multi_action(**options)
start_branch_name: nil, start_project: project)
multi_action(
user: user,
message: message,
branch_name: branch_name,
author_email: author_email,
author_name: author_name,
start_branch_name: start_branch_name,
start_project: start_project,
actions: [{ action: :delete,
file_path: path }])
end end
# rubocop:enable Metrics/ParameterLists
# rubocop:disable Metrics/ParameterLists # rubocop:disable Metrics/ParameterLists
def multi_action( def multi_action(
user:, branch_name:, message:, actions:, user:, branch_name:, message:, actions:,
author_email: nil, author_name: nil, author_email: nil, author_name: nil,
start_branch_name: nil, start_project: project) start_branch_name: nil, start_project: project)
GitOperationService.new(user, self).with_branch( GitOperationService.new(user, self).with_branch(
branch_name, branch_name,
start_branch_name: start_branch_name, start_branch_name: start_branch_name,
start_project: start_project) do |start_commit| start_project: start_project) do |start_commit|
index = rugged.index
parents = if start_commit index = Gitlab::Git::Index.new(raw_repository)
index.read_tree(start_commit.raw_commit.tree)
[start_commit.sha]
else
[]
end
actions.each do |act| if start_commit
git_action(index, act) index.read_tree(start_commit.raw_commit.tree)
parents = [start_commit.sha]
else
parents = []
end
actions.each do |options|
index.public_send(options.delete(:action), options)
end end
options = { options = {
tree: index.write_tree(rugged), tree: index.write_tree,
message: message, message: message,
parents: parents parents: parents
} }
...@@ -1166,30 +1091,6 @@ class Repository ...@@ -1166,30 +1091,6 @@ class Repository
blob_data_at(sha, '.gitlab-ci.yml') blob_data_at(sha, '.gitlab-ci.yml')
end end
protected
def tree_entry_at(branch_name, path)
branch_exists?(branch_name) &&
# tree_entry is private
raw_repository.send(:tree_entry, commit(branch_name), path)
end
def check_tree_entry_for_dir(branch_name, path)
return unless branch_exists?(branch_name)
entry = tree_entry_at(branch_name, path)
return unless entry
if entry[:type] == :blob
raise Gitlab::Git::Repository::InvalidBlobName.new(
"Directory already exists as a file")
else
raise Gitlab::Git::Repository::InvalidBlobName.new(
"Directory already exists")
end
end
private private
def blob_data_at(sha, path) def blob_data_at(sha, path)
...@@ -1200,58 +1101,6 @@ class Repository ...@@ -1200,58 +1101,6 @@ class Repository
blob.data blob.data
end end
def git_action(index, action)
path = normalize_path(action[:file_path])
if action[:action] == :move
previous_path = normalize_path(action[:previous_path])
end
case action[:action]
when :create, :update, :move
mode =
case action[:action]
when :update
index.get(path)[:mode]
when :move
index.get(previous_path)[:mode]
end
mode ||= 0o100644
index.remove(previous_path) if action[:action] == :move
content = if action[:encoding] == 'base64'
Base64.decode64(action[:content])
else
action[:content]
end
detect = CharlockHolmes::EncodingDetector.new.detect(content) if content
unless detect && detect[:type] == :binary
# When writing to the repo directly as we are doing here,
# the `core.autocrlf` config isn't taken into account.
content.gsub!("\r\n", "\n") if self.autocrlf
end
oid = rugged.write(content, :blob)
index.add(path: path, oid: oid, mode: mode)
when :delete
index.remove(path)
end
end
def normalize_path(path)
pathname = Gitlab::Git::PathHelper.normalize_path(path)
if pathname.each_filename.include?('..')
raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path')
end
pathname.to_s
end
def refs_directory_exists? def refs_directory_exists?
return false unless path_with_namespace return false unless path_with_namespace
......
...@@ -81,7 +81,6 @@ class User < ActiveRecord::Base ...@@ -81,7 +81,6 @@ class User < ActiveRecord::Base
has_many :authorized_projects, through: :project_authorizations, source: :project has_many :authorized_projects, through: :project_authorizations, source: :project
has_many :snippets, dependent: :destroy, foreign_key: :author_id has_many :snippets, dependent: :destroy, foreign_key: :author_id
has_many :issues, dependent: :destroy, foreign_key: :author_id
has_many :notes, dependent: :destroy, foreign_key: :author_id has_many :notes, dependent: :destroy, foreign_key: :author_id
has_many :merge_requests, dependent: :destroy, foreign_key: :author_id has_many :merge_requests, dependent: :destroy, foreign_key: :author_id
has_many :events, dependent: :destroy, foreign_key: :author_id has_many :events, dependent: :destroy, foreign_key: :author_id
...@@ -99,6 +98,11 @@ class User < ActiveRecord::Base ...@@ -99,6 +98,11 @@ class User < ActiveRecord::Base
has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue" has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue"
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest"
# Issues that a user owns are expected to be moved to the "ghost" user before
# the user is destroyed. If the user owns any issues during deletion, this
# should be treated as an exceptional condition.
has_many :issues, dependent: :restrict_with_exception, foreign_key: :author_id
# #
# Validations # Validations
# #
...@@ -120,6 +124,7 @@ class User < ActiveRecord::Base ...@@ -120,6 +124,7 @@ class User < ActiveRecord::Base
validate :unique_email, if: ->(user) { user.email_changed? } validate :unique_email, if: ->(user) { user.email_changed? }
validate :owns_notification_email, if: ->(user) { user.notification_email_changed? } validate :owns_notification_email, if: ->(user) { user.notification_email_changed? }
validate :owns_public_email, if: ->(user) { user.public_email_changed? } validate :owns_public_email, if: ->(user) { user.public_email_changed? }
validate :ghost_users_must_be_blocked
validates :avatar, file_size: { maximum: 200.kilobytes.to_i } validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
before_validation :generate_password, on: :create before_validation :generate_password, on: :create
...@@ -334,9 +339,15 @@ class User < ActiveRecord::Base ...@@ -334,9 +339,15 @@ class User < ActiveRecord::Base
def reference_pattern def reference_pattern
%r{ %r{
#{Regexp.escape(reference_prefix)} #{Regexp.escape(reference_prefix)}
(?<user>#{Gitlab::Regex::NAMESPACE_REF_REGEX_STR}) (?<user>#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR})
}x }x
end end
# Return (create if necessary) the ghost user. The ghost user
# owns records previously belonging to deleted users.
def ghost
User.find_by_ghost(true) || create_ghost_user
end
end end
# #
...@@ -435,6 +446,12 @@ class User < ActiveRecord::Base ...@@ -435,6 +446,12 @@ class User < ActiveRecord::Base
errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email) errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email)
end end
def ghost_users_must_be_blocked
if ghost? && !blocked?
errors.add(:ghost, 'cannot be enabled for a user who is not blocked.')
end
end
def update_emails_with_primary_email def update_emails_with_primary_email
primary_email_record = emails.find_by(email: email) primary_email_record = emails.find_by(email: email)
if primary_email_record if primary_email_record
...@@ -999,4 +1016,40 @@ class User < ActiveRecord::Base ...@@ -999,4 +1016,40 @@ class User < ActiveRecord::Base
super super
end end
end end
def self.create_ghost_user
# Since we only want a single ghost user in an instance, we use an
# exclusive lease to ensure than this block is never run concurrently.
lease_key = "ghost_user_creation"
lease = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.minute.to_i)
until uuid = lease.try_obtain
# Keep trying until we obtain the lease. To prevent hammering Redis too
# much we'll wait for a bit between retries.
sleep(1)
end
# Recheck if a ghost user is already present. One might have been
# added between the time we last checked (first line of this method)
# and the time we acquired the lock.
ghost_user = User.find_by_ghost(true)
return ghost_user if ghost_user.present?
uniquify = Uniquify.new
username = uniquify.string("ghost") { |s| User.find_by_username(s) }
email = uniquify.string(-> (n) { "ghost#{n}@example.com" }) do |s|
User.find_by_email(s)
end
bio = 'This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.'
User.create(
username: username, password: Devise.friendly_token, bio: bio,
email: email, name: "Ghost User", state: :blocked, ghost: true
)
ensure
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
end
end end
...@@ -3,6 +3,14 @@ class UserPolicy < BasePolicy ...@@ -3,6 +3,14 @@ class UserPolicy < BasePolicy
def rules def rules
can! :read_user if @user || !restricted_public_level? can! :read_user if @user || !restricted_public_level?
if @user
if @user.admin? || @subject == @user
can! :destroy_user
end
cannot! :destroy_user if @subject.ghost?
end
end end
def restricted_public_level? def restricted_public_level?
......
module Files module Files
class CreateDirService < Files::BaseService class CreateDirService < Files::BaseService
def commit def commit
repository.commit_dir( repository.create_dir(
current_user, current_user,
@file_path, @file_path,
message: @commit_message, message: @commit_message,
......
module Files module Files
class CreateService < Files::BaseService class CreateService < Files::BaseService
def commit def commit
repository.commit_file( repository.create_file(
current_user, current_user,
@file_path, @file_path,
@file_content, @file_content,
message: @commit_message, message: @commit_message,
branch_name: @target_branch, branch_name: @target_branch,
update: false,
author_email: @author_email, author_email: @author_email,
author_name: @author_name, author_name: @author_name,
start_project: @start_project, start_project: @start_project,
...@@ -17,6 +16,10 @@ module Files ...@@ -17,6 +16,10 @@ module Files
def validate def validate
super super
if @file_content.nil?
raise_error("You must provide content.")
end
if @file_path =~ Gitlab::Regex.directory_traversal_regex if @file_path =~ Gitlab::Regex.directory_traversal_regex
raise_error( raise_error(
'Your changes could not be committed, because the file name ' + 'Your changes could not be committed, because the file name ' +
......
module Files module Files
class DestroyService < Files::BaseService class DestroyService < Files::BaseService
def commit def commit
repository.remove_file( repository.delete_file(
current_user, current_user,
@file_path, @file_path,
message: @commit_message, message: @commit_message,
......
...@@ -2,6 +2,8 @@ module Files ...@@ -2,6 +2,8 @@ module Files
class MultiService < Files::BaseService class MultiService < Files::BaseService
class FileChangedError < StandardError; end class FileChangedError < StandardError; end
ACTIONS = %w[create update delete move].freeze
def commit def commit
repository.multi_action( repository.multi_action(
user: current_user, user: current_user,
...@@ -21,10 +23,19 @@ module Files ...@@ -21,10 +23,19 @@ module Files
super super
params[:actions].each_with_index do |action, index| params[:actions].each_with_index do |action, index|
if ACTIONS.include?(action[:action].to_s)
action[:action] = action[:action].to_sym
else
raise_error("Unknown action type `#{action[:action]}`.")
end
unless action[:file_path].present? unless action[:file_path].present?
raise_error("You must specify a file_path.") raise_error("You must specify a file_path.")
end end
action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/')
action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/')
regex_check(action[:file_path]) regex_check(action[:file_path])
regex_check(action[:previous_path]) if action[:previous_path] regex_check(action[:previous_path]) if action[:previous_path]
...@@ -43,8 +54,6 @@ module Files ...@@ -43,8 +54,6 @@ module Files
validate_delete(action) validate_delete(action)
when :move when :move
validate_move(action, index) validate_move(action, index)
else
raise_error("Unknown action type `#{action[:action]}`.")
end end
end end
end end
...@@ -92,6 +101,20 @@ module Files ...@@ -92,6 +101,20 @@ module Files
if repository.blob_at_branch(params[:branch], action[:file_path]) if repository.blob_at_branch(params[:branch], action[:file_path])
raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.") raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.")
end end
if action[:content].nil?
raise_error("You must provide content.")
end
end
def validate_update(action)
if action[:content].nil?
raise_error("You must provide content.")
end
if file_has_changed?
raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.")
end
end end
def validate_delete(action) def validate_delete(action)
...@@ -114,11 +137,5 @@ module Files ...@@ -114,11 +137,5 @@ module Files
params[:actions][index][:content] = blob.data params[:actions][index][:content] = blob.data
end end
end end
def validate_update(action)
if file_has_changed?
raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.")
end
end
end end
end end
...@@ -18,6 +18,10 @@ module Files ...@@ -18,6 +18,10 @@ module Files
def validate def validate
super super
if @file_content.nil?
raise_error("You must provide content.")
end
if file_has_changed? if file_has_changed?
raise FileChangedError.new("You are attempting to update a file that has changed since you started editing it.") raise FileChangedError.new("You are attempting to update a file that has changed since you started editing it.")
end end
......
...@@ -7,7 +7,7 @@ module Users ...@@ -7,7 +7,7 @@ module Users
end end
def execute(user, options = {}) def execute(user, options = {})
unless current_user.admin? || current_user == user unless Ability.allowed?(current_user, :destroy_user, user)
raise Gitlab::Access::AccessDeniedError, "#{current_user} tried to destroy user #{user}!" raise Gitlab::Access::AccessDeniedError, "#{current_user} tried to destroy user #{user}!"
end end
...@@ -26,6 +26,8 @@ module Users ...@@ -26,6 +26,8 @@ module Users
::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute ::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute
end end
move_issues_to_ghost_user(user)
# Destroy the namespace after destroying the user since certain methods may depend on the namespace existing # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
namespace = user.namespace namespace = user.namespace
user_data = user.destroy user_data = user.destroy
...@@ -33,5 +35,22 @@ module Users ...@@ -33,5 +35,22 @@ module Users
user_data user_data
end end
private
def move_issues_to_ghost_user(user)
# Block the user before moving issues to prevent a data race.
# If the user creates an issue after `move_issues_to_ghost_user`
# runs and before the user is destroyed, the destroy will fail with
# an exception. We block the user so that issues can't be created
# after `move_issues_to_ghost_user` runs and before the destroy happens.
user.block
ghost_user = User.ghost
user.issues.update_all(author_id: ghost_user.id)
user.reload
end
end end
end end
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
- if user.access_locked? - if user.access_locked?
%li %li
= link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' } = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' }
- if user.can_be_removed? - if user.can_be_removed? && can?(current_user, :destroy_user, @user)
%li.divider %li.divider
%li %li
= link_to 'Delete User', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Consider cancelling this deletion and blocking the user instead. Are you sure?" }, = link_to 'Delete User', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Consider cancelling this deletion and blocking the user instead. Are you sure?" },
......
...@@ -173,7 +173,7 @@ ...@@ -173,7 +173,7 @@
.panel-heading .panel-heading
Remove user Remove user
.panel-body .panel-body
- if @user.can_be_removed? - if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p Deleting a user has the following effects: %p Deleting a user has the following effects:
%ul %ul
%li All user content like authored issues, snippets, comments will be removed %li All user content like authored issues, snippets, comments will be removed
...@@ -189,3 +189,6 @@ ...@@ -189,3 +189,6 @@
%strong= @user.solo_owned_groups.map(&:name).join(', ') %strong= @user.solo_owned_groups.map(&:name).join(', ')
%p %p
You must transfer ownership or delete these groups before you can delete this user. You must transfer ownership or delete these groups before you can delete this user.
- else
%p
You don't have access to delete this user.
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
- page_title "Projects" - page_title "Projects"
- header_title "Projects", dashboard_projects_path - header_title "Projects", dashboard_projects_path
.user-callout{ 'callout-svg' => custom_icon('icon_customization') }
- if @projects.any? || params[:filter_projects] - if @projects.any? || params[:filter_projects]
= render 'dashboard/projects_head' = render 'dashboard/projects_head'
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
= f.text_field :name, class: "form-control top", required: true, title: "This field is required." = f.text_field :name, class: "form-control top", required: true, title: "This field is required."
.username.form-group .username.form-group
= f.label :username = f.label :username
= f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE, required: true, title: 'Please create a username with only alphanumeric characters.' = f.text_field :username, class: "form-control middle", pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS, required: true, title: 'Please create a username with only alphanumeric characters.'
%p.validation-error.hide Username is already taken. %p.validation-error.hide Username is already taken.
%p.validation-success.hide Username is available. %p.validation-success.hide Username is available.
%p.validation-pending.hide Checking username availability... %p.validation-pending.hide Checking username availability...
......
...@@ -115,7 +115,7 @@ ...@@ -115,7 +115,7 @@
%h4.prepend-top-0.danger-title %h4.prepend-top-0.danger-title
Remove account Remove account
.col-lg-9 .col-lg-9
- if @user.can_be_removed? - if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p %p
Deleting an account has the following effects: Deleting an account has the following effects:
%ul %ul
...@@ -131,4 +131,7 @@ ...@@ -131,4 +131,7 @@
%strong= @user.solo_owned_groups.map(&:name).join(', ') %strong= @user.solo_owned_groups.map(&:name).join(', ')
%p %p
You must transfer ownership or delete these groups before you can delete your account. You must transfer ownership or delete these groups before you can delete your account.
- else
%p
You don't have access to delete this user.
.append-bottom-default .append-bottom-default
...@@ -101,5 +101,3 @@ ...@@ -101,5 +101,3 @@
$("#created-personal-access-token").click(function() { $("#created-personal-access-token").click(function() {
this.select(); this.select();
}); });
$("#created-personal-access-token").effect('highlight', { color: '#ffff99' }, 2000);
...@@ -19,10 +19,10 @@ ...@@ -19,10 +19,10 @@
= line_content = line_content
- when :parallel - when :parallel
%td.old_line.diff-line-num{ data: { linenumber: line_old } } %td.old_line.diff-line-num{ data: { linenumber: line_old } }
= link_to raw(line_old), "##{line_old}" %a{ href: "##{line_old}", data: { linenumber: line_old } }
= line_content = line_content
%td.new_line.diff-line-num{ data: { linenumber: line_new } } %td.new_line.diff-line-num{ data: { linenumber: line_new } }
= link_to raw(line_new), "##{line_new}" %a{ href: "##{line_new}", data: { linenumber: line_new } }
= line_content = line_content
- if @form.unfold? && @form.bottom? && @form.to < @blob.lines.size - if @form.unfold? && @form.bottom? && @form.to < @blob.lines.size
......
...@@ -8,7 +8,6 @@ ...@@ -8,7 +8,6 @@
%script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board" %script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
%script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list" %script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list"
%script#js-board-list-card{ type: "text/x-template" }= render "projects/boards/components/card"
= render "projects/issues/head" = render "projects/issues/head"
......
%li.card{ ":class" => '{ "user-can-drag": !disabled && issue.id, "is-disabled": disabled || !issue.id, "is-active": issueDetailVisible }',
":index" => "index",
":data-issue-id" => "issue.id",
"@mousedown" => "mouseDown",
"@mousemove" => "mouseMove",
"@mouseup" => "showIssue($event)" }
%issue-card-inner{ ":list" => "list",
":issue" => "issue",
":issue-link-base" => "issueLinkBase",
":root-path" => "rootPath" }
...@@ -12,12 +12,16 @@ ...@@ -12,12 +12,16 @@
.form-group .form-group
= label_tag :branch_name, nil, class: 'control-label' = label_tag :branch_name, nil, class: 'control-label'
.col-sm-10 .col-sm-10
= text_field_tag :branch_name, params[:branch_name], required: true, tabindex: 1, autofocus: true, class: 'form-control js-branch-name' = text_field_tag :branch_name, params[:branch_name], required: true, autofocus: true, class: 'form-control js-branch-name'
.help-block.text-danger.js-branch-name-error .help-block.text-danger.js-branch-name-error
.form-group .form-group
= label_tag :ref, 'Create from', class: 'control-label' = label_tag :ref, 'Create from', class: 'control-label'
.col-sm-10 .col-sm-10
= text_field_tag :ref, params[:ref] || @project.default_branch, required: true, tabindex: 2, class: 'form-control' = hidden_field_tag :ref, params[:ref] || @project.default_branch
= dropdown_tag(params[:ref] || @project.default_branch,
options: { toggle_class: 'js-branch-select wide',
filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search branches",
data: { selected: params[:ref] || @project.default_branch, field_name: 'ref' } })
.help-block Existing branch name, tag, or commit SHA .help-block Existing branch name, tag, or commit SHA
.form-actions .form-actions
= button_tag 'Create branch', class: 'btn btn-create', tabindex: 3 = button_tag 'Create branch', class: 'btn btn-create', tabindex: 3
......
...@@ -10,10 +10,10 @@ ...@@ -10,10 +10,10 @@
- if diff_file.renamed_file - if diff_file.renamed_file
- old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
%strong.file-title-name.has-tooltip{ data: { title: old_path, container: 'body' } } %strong.file-title-name.has-tooltip{ data: { title: diff_file.old_path, container: 'body' } }
= old_path = old_path
&rarr; &rarr;
%strong.file-title-name.has-tooltip{ data: { title: new_path, container: 'body' } } %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } }
= new_path = new_path
- else - else
%strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } } %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } }
......
...@@ -120,7 +120,7 @@ ...@@ -120,7 +120,7 @@
.form-group .form-group
- if @project.avatar? - if @project.avatar?
.avatar-container.s160 .avatar-container.s160
= project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160') = project_icon(@project.full_path, alt: '', class: 'avatar project-avatar s160')
%p.light %p.light
- if @project.avatar_in_git - if @project.avatar_in_git
Project avatar in repository: #{ @project.avatar_in_git } Project avatar in repository: #{ @project.avatar_in_git }
......
...@@ -9,7 +9,11 @@ ...@@ -9,7 +9,11 @@
.form-group .form-group
= f.label :ref, 'Create for', class: 'control-label' = f.label :ref, 'Create for', class: 'control-label'
.col-sm-10 .col-sm-10
= f.text_field :ref, required: true, tabindex: 2, class: 'form-control js-branch-name ui-autocomplete-input', autocomplete: :false, id: :ref = hidden_field_tag 'pipeline[ref]', params[:ref] || @project.default_branch
= dropdown_tag(params[:ref] || @project.default_branch,
options: { toggle_class: 'js-branch-select wide',
filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search branches",
data: { selected: params[:ref] || @project.default_branch, field_name: 'pipeline[ref]' } })
.help-block Existing branch name, tag .help-block Existing branch name, tag
.form-actions .form-actions
= f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3 = f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
%strong= parent.full_path + '/' %strong= parent.full_path + '/'
= f.text_field :path, placeholder: 'open-source', class: 'form-control', = f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true, autofocus: local_assigns[:autofocus] || false, required: true,
pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_SIMPLE, pattern: Gitlab::Regex::NAMESPACE_REGEX_STR_JS,
title: 'Please choose a group name with no special characters.' title: 'Please choose a group name with no special characters.'
- if parent - if parent
= f.hidden_field :parent_id, value: parent.id = f.hidden_field :parent_id, value: parent.id
......
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 112 90" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="none" fill-rule="evenodd"><rect width="112" height="90" fill="#fff" rx="6"/><path fill="#eee" fill-rule="nonzero" d="m4 6.01v77.98c0 1.11.899 2.01 2 2.01h100c1.105 0 2-.898 2-2.01v-77.98c0-1.11-.899-2.01-2-2.01h-100c-1.105 0-2 .898-2 2.01m-4 0c0-3.319 2.686-6.01 6-6.01h100c3.315 0 6 2.694 6 6.01v77.98c0 3.319-2.686 6.01-6 6.01h-100c-3.315 0-6-2.694-6-6.01v-77.98"/><g transform="translate(26 35)"><rect width="4" height="39" x="5" fill="#eee" rx="2" id="0"/><rect width="4" height="21" x="5" y="18" fill="#fef0ea" rx="2"/><circle cx="7" cy="13" r="5" fill="#fff"/><path fill="#fb722e" fill-rule="nonzero" d="m7 20c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7m0-4c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3"/></g><g transform="translate(49 35)"><use xlink:href="#0"/><rect width="4" height="21" x="5" y="18" fill="#b5a7dd" rx="2"/><circle cx="7" cy="25" r="5" fill="#fff"/><path fill="#6b4fbb" fill-rule="nonzero" d="m7 32c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7m0-4c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3"/></g><g transform="translate(72 33)"><rect width="4" height="39" x="5" y="2" fill="#eee" rx="2"/><rect width="4" height="34" x="5" y="7" fill="#fef0ea" rx="2"/><circle cx="7" cy="7" r="5" fill="#fff"/><path fill="#fb722e" fill-rule="nonzero" d="m7 14c-3.866 0-7-3.134-7-7 0-3.866 3.134-7 7-7 3.866 0 7 3.134 7 7 0 3.866-3.134 7-7 7m0-4c1.657 0 3-1.343 3-3 0-1.657-1.343-3-3-3-1.657 0-3 1.343-3 3 0 1.657 1.343 3 3 3"/></g><g fill="#6b4fbb"><circle cx="13.5" cy="11.5" r="2.5"/><circle cx="23.5" cy="11.5" r="2.5" opacity=".5"/><circle cx="33.5" cy="11.5" r="2.5" opacity=".5"/></g><path fill="#eee" d="m0 19h111v4h-111z"/></g></svg>
...@@ -173,7 +173,7 @@ ...@@ -173,7 +173,7 @@
:javascript :javascript
gl.IssuableResource = new gl.SubbableResource('#{issuable_json_path(issuable)}'); gl.IssuableResource = new gl.SubbableResource('#{issuable_json_path(issuable)}');
new gl.IssuableTimeTracking("#{escape_javascript(serialize_issuable(issuable))}"); new gl.IssuableTimeTracking("#{escape_javascript(serialize_issuable(issuable))}");
new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}'); new MilestoneSelect('{"full_path":"#{@project.full_path}"}');
new LabelsSelect(); new LabelsSelect();
new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}'); new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}');
gl.Subscription.bindAll('.subscription'); gl.Subscription.bindAll('.subscription');
......
...@@ -98,6 +98,7 @@ ...@@ -98,6 +98,7 @@
Snippets Snippets
%div{ class: container_class } %div{ class: container_class }
.user-callout{ 'callout-svg' => custom_icon('icon_customization') }
.tab-content .tab-content
#activity.tab-pane #activity.tab-pane
.row-content-block.calender-block.white.second-block.hidden-xs .row-content-block.calender-block.white.second-block.hidden-xs
......
---
title: Deleting a user doesn't delete issues they've created/are assigned to
merge_request: 7393
author:
---
title: Make Git history follow renames again by performing the --skip in Ruby
merge_request:
author:
---
title: 'Add performance query regression fix for !9088 affecting #27267'
merge_request:
author:
---
title: Remove markup that was showing in tooltip for renamed files
merge_request: 9374
author:
---
title: Fixes includes line number during unfold copy n paste in parallel diff view
merge_request: 9365
author:
---
title: 'API: Remove /groups/owned endpoint'
merge_request: 9505
author: Robert Schilling
---
title: Add Runner's registration/deletion v4 API
merge_request: 9246
author:
---
title: Removes label when moving issue to another list that it is currently in
merge_request:
author:
---
title: Removed jQuery UI highlight & autocomplete
merge_request:
author:
---
title: Bump Hashie to 3.5.5 and omniauth to 1.4.2 to eliminate warning noise
merge_request:
author:
---
title: 'API: Return 400 for all validation erros in the mebers API'
merge_request: 9523
author: Robert Schilling
---
title: update Vue to v2.1.10
merge_request: 9386
author:
---
title: Added user callouts to the projects dashboard and user profile
merge_request:
author:
...@@ -87,7 +87,7 @@ var config = { ...@@ -87,7 +87,7 @@ var config = {
'bootstrap/js': 'bootstrap-sass/assets/javascripts/bootstrap', 'bootstrap/js': 'bootstrap-sass/assets/javascripts/bootstrap',
'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'), 'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'),
'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'),
'vue$': IS_PRODUCTION ? 'vue/dist/vue.min.js' : 'vue/dist/vue.js', 'vue$': 'vue/dist/vue.common.js',
} }
} }
} }
......
...@@ -155,17 +155,9 @@ class Gitlab::Seeder::CycleAnalytics ...@@ -155,17 +155,9 @@ class Gitlab::Seeder::CycleAnalytics
issue.project.repository.add_branch(@user, branch_name, 'master') issue.project.repository.add_branch(@user, branch_name, 'master')
options = { commit_sha = issue.project.repository.create_file(@user, filename, "content", options, message: "Commit for ##{issue.iid}", branch_name: branch_name)
committer: issue.project.repository.user_to_committer(@user),
author: issue.project.repository.user_to_committer(@user),
commit: { message: "Commit for ##{issue.iid}", branch: branch_name, update_ref: true },
file: { content: "content", path: filename, update: false }
}
commit_sha = Gitlab::Git::Blob.commit(issue.project.repository, options)
issue.project.repository.commit(commit_sha) issue.project.repository.commit(commit_sha)
GitPushService.new(issue.project, GitPushService.new(issue.project,
@user, @user,
oldrev: issue.project.repository.commit("master").sha, oldrev: issue.project.repository.commit("master").sha,
......
class AddColumnGhostToUsers < ActiveRecord::Migration
DOWNTIME = false
def up
add_column :users, :ghost, :boolean
end
def down
remove_column :users, :ghost
end
end
...@@ -1291,10 +1291,11 @@ ActiveRecord::Schema.define(version: 20170216141440) do ...@@ -1291,10 +1291,11 @@ ActiveRecord::Schema.define(version: 20170216141440) do
t.datetime "otp_grace_period_started_at" t.datetime "otp_grace_period_started_at"
t.boolean "ldap_email", default: false, null: false t.boolean "ldap_email", default: false, null: false
t.boolean "external", default: false t.boolean "external", default: false
t.string "organization"
t.string "incoming_email_token" t.string "incoming_email_token"
t.string "organization"
t.boolean "authorized_projects_populated" t.boolean "authorized_projects_populated"
t.boolean "notified_of_own_activity", default: false, null: false t.boolean "notified_of_own_activity", default: false, null: false
t.boolean "ghost"
end end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
......
...@@ -13,7 +13,7 @@ To enable the GitLab monitor exporter: ...@@ -13,7 +13,7 @@ To enable the GitLab monitor exporter:
1. Add or find and uncomment the following line, making sure it's set to `true`: 1. Add or find and uncomment the following line, making sure it's set to `true`:
```ruby ```ruby
gitlab_monitor_exporter['enable'] = true gitlab_monitor['enable'] = true
``` ```
1. Save the file and [reconfigure GitLab][reconfigure] for the changes to 1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
......
...@@ -26,22 +26,24 @@ it works. ...@@ -26,22 +26,24 @@ it works.
--- ---
In the case of custom domains, the Pages daemon needs to listen on ports `80` In the case of [custom domains](#custom-domains) (but not
and/or `443`. For that reason, there is some flexibility in the way which you [wildcard domains](#wildcard-domains)), the Pages daemon needs to listen on
can set it up: ports `80` and/or `443`. For that reason, there is some flexibility in the way
which you can set it up:
1. Run the pages daemon in the same server as GitLab, listening on a secondary IP. 1. Run the Pages daemon in the same server as GitLab, listening on a secondary IP.
1. Run the pages daemon in a separate server. In that case, the 1. Run the Pages daemon in a separate server. In that case, the
[Pages path](#change-storage-path) must also be present in the server that [Pages path](#change-storage-path) must also be present in the server that
the pages daemon is installed, so you will have to share it via network. the Pages daemon is installed, so you will have to share it via network.
1. Run the pages daemon in the same server as GitLab, listening on the same IP 1. Run the Pages daemon in the same server as GitLab, listening on the same IP
but on different ports. In that case, you will have to proxy the traffic with but on different ports. In that case, you will have to proxy the traffic with
a loadbalancer. If you choose that route note that you should use TCP load a loadbalancer. If you choose that route note that you should use TCP load
balancing for HTTPS. If you use TLS-termination (HTTPS-load balancing) the balancing for HTTPS. If you use TLS-termination (HTTPS-load balancing) the
pages will not be able to be served with user provided certificates. For pages will not be able to be served with user provided certificates. For
HTTP it's OK to use HTTP or TCP load balancing. HTTP it's OK to use HTTP or TCP load balancing.
In this document, we will proceed assuming the first option. In this document, we will proceed assuming the first option. If you are not
supporting custom domains a secondary IP is not needed.
## Prerequisites ## Prerequisites
...@@ -54,6 +56,7 @@ Before proceeding with the Pages configuration, you will need to: ...@@ -54,6 +56,7 @@ Before proceeding with the Pages configuration, you will need to:
serve Pages under HTTPS. serve Pages under HTTPS.
1. (Optional but recommended) Enable [Shared runners](../../ci/runners/README.md) 1. (Optional but recommended) Enable [Shared runners](../../ci/runners/README.md)
so that your users don't have to bring their own. so that your users don't have to bring their own.
1. (Only for custom domains) Have a **secondary IP**.
### DNS configuration ### DNS configuration
...@@ -150,7 +153,7 @@ that without TLS certificates. ...@@ -150,7 +153,7 @@ that without TLS certificates.
> >
URL scheme: `http://page.example.io` and `http://domain.com` URL scheme: `http://page.example.io` and `http://domain.com`
In that case, the pages daemon is running, Nginx still proxies requests to In that case, the Pages daemon is running, Nginx still proxies requests to
the daemon but the daemon is also able to receive requests from the outside the daemon but the daemon is also able to receive requests from the outside
world. Custom domains are supported, but no TLS. world. Custom domains are supported, but no TLS.
...@@ -179,7 +182,7 @@ world. Custom domains are supported, but no TLS. ...@@ -179,7 +182,7 @@ world. Custom domains are supported, but no TLS.
> >
URL scheme: `https://page.example.io` and `https://domain.com` URL scheme: `https://page.example.io` and `https://domain.com`
In that case, the pages daemon is running, Nginx still proxies requests to In that case, the Pages daemon is running, Nginx still proxies requests to
the daemon but the daemon is also able to receive requests from the outside the daemon but the daemon is also able to receive requests from the outside
world. Custom domains and TLS are supported. world. Custom domains and TLS are supported.
......
...@@ -69,7 +69,9 @@ please consult [RFC 5322](https://tools.ietf.org/html/rfc5322#section-3.6.4). ...@@ -69,7 +69,9 @@ please consult [RFC 5322](https://tools.ietf.org/html/rfc5322#section-3.6.4).
If you want to use Gmail / Google Apps with Reply by email, make sure you have If you want to use Gmail / Google Apps with Reply by email, make sure you have
[IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) [IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018)
and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255). and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255)
or [turn-on 2-step validation](https://support.google.com/accounts/answer/185839)
and use [an application password](https://support.google.com/mail/answer/185833).
To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
[Postfix setup documentation](reply_by_email_postfix_setup.md). [Postfix setup documentation](reply_by_email_postfix_setup.md).
......
...@@ -14,6 +14,7 @@ Parameters: ...@@ -14,6 +14,7 @@ Parameters:
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` | | `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` | | `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
| `statistics` | boolean | no | Include group statistics (admins only) | | `statistics` | boolean | no | Include group statistics (admins only) |
| `owned` | boolean | no | Limit by groups owned by the current user |
``` ```
GET /groups GET /groups
...@@ -40,20 +41,6 @@ GET /groups ...@@ -40,20 +41,6 @@ GET /groups
You can search for groups by name or path, see below. You can search for groups by name or path, see below.
## List owned groups
Get a list of groups which are owned by the authenticated user.
```
GET /groups/owned
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `statistics` | boolean | no | Include group statistics |
## List a group's projects ## List a group's projects
Get a list of projects in this group. Get a list of projects in this group.
......
...@@ -41,5 +41,6 @@ changes are in V4: ...@@ -41,5 +41,6 @@ changes are in V4:
- Renamed `branch_name` to `branch` on DELETE `id/repository/branches/:branch` response [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936) - Renamed `branch_name` to `branch` on DELETE `id/repository/branches/:branch` response [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936)
- Remove `public` param from create and edit actions of projects [!8736](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8736) - Remove `public` param from create and edit actions of projects [!8736](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8736)
- Notes do not return deprecated field `upvote` and `downvote` [!9384](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9384) - Notes do not return deprecated field `upvote` and `downvote` [!9384](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9384)
- Return HTTP status code `400` for all validation errors when creating or updating a member instead of sometimes `422` error. [!9523](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9523)
- Remove `GET /groups/owned`. Use `GET /groups?owned=true` instead [!9505](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9505)
- Return 202 with JSON body on async removals on V4 API (DELETE `/projects/:id/repository/merged_branches` and DELETE `/projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449) - Return 202 with JSON body on async removals on V4 API (DELETE `/projects/:id/repository/merged_branches` and DELETE `/projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449)
...@@ -1018,7 +1018,7 @@ A simple example: ...@@ -1018,7 +1018,7 @@ A simple example:
```yaml ```yaml
job1: job1:
coverage: /Code coverage: \d+\.\d+/ coverage: '/Code coverage: \d+\.\d+/'
``` ```
## Git Strategy ## Git Strategy
......
This diff is collapsed.
...@@ -54,7 +54,7 @@ for initial settings. ...@@ -54,7 +54,7 @@ for initial settings.
gitlab_rails['omniauth_providers'] = [ gitlab_rails['omniauth_providers'] = [
{ {
"name" => "auth0", "name" => "auth0",
"args" => { client_id: 'YOUR_AUTH0_CLIENT_ID'', "args" => { client_id: 'YOUR_AUTH0_CLIENT_ID',
client_secret: 'YOUR_AUTH0_CLIENT_SECRET', client_secret: 'YOUR_AUTH0_CLIENT_SECRET',
namespace: 'YOUR_AUTH0_DOMAIN' namespace: 'YOUR_AUTH0_DOMAIN'
} }
......
...@@ -74,7 +74,7 @@ in your SAML IdP: ...@@ -74,7 +74,7 @@ in your SAML IdP:
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp', idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com', issuer: 'https://gitlab.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
}, },
label: 'Company Login' # optional label for SAML login button, defaults to "Saml" label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
} }
...@@ -91,7 +91,7 @@ in your SAML IdP: ...@@ -91,7 +91,7 @@ in your SAML IdP:
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp', idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com', issuer: 'https://gitlab.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
}, },
label: 'Company Login' # optional label for SAML login button, defaults to "Saml" label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
} }
...@@ -172,7 +172,7 @@ tell GitLab which groups are external via the `external_groups:` element: ...@@ -172,7 +172,7 @@ tell GitLab which groups are external via the `external_groups:` element:
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp', idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com', issuer: 'https://gitlab.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient' name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
} } } }
``` ```
...@@ -227,7 +227,7 @@ args: { ...@@ -227,7 +227,7 @@ args: {
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp', idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com', issuer: 'https://gitlab.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
attribute_statements: { email: ['EmailAddress'] } attribute_statements: { email: ['EmailAddress'] }
} }
``` ```
...@@ -245,7 +245,7 @@ args: { ...@@ -245,7 +245,7 @@ args: {
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp', idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com', issuer: 'https://gitlab.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient', name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
attribute_statements: { email: ['EmailAddress'] }, attribute_statements: { email: ['EmailAddress'] },
allowed_clock_drift: 1 # for one second clock drift allowed_clock_drift: 1 # for one second clock drift
} }
......
...@@ -16,8 +16,6 @@ that this setting is set for each job. ...@@ -16,8 +16,6 @@ that this setting is set for each job.
1. Hit **Save** for the changes to take effect. 1. Hit **Save** for the changes to take effect.
[art-yml]: ../../../administration/build_artifacts
## Default artifacts expiration ## Default artifacts expiration
The default expiration time of the [job artifacts][art-yml] can be set in The default expiration time of the [job artifacts][art-yml] can be set in
...@@ -36,5 +34,5 @@ expiration. ...@@ -36,5 +34,5 @@ expiration.
1. Hit **Save** for the changes to take effect. 1. Hit **Save** for the changes to take effect.
[art-yml]: ../../../administration/job_artifacts [art-yml]: ../../../administration/job_artifacts.md
[duration-syntax]: ../../../ci/yaml/README#artifactsexpire_in [duration-syntax]: ../../../ci/yaml/README.md#artifactsexpire_in
...@@ -24,23 +24,24 @@ There, you will see a checkbox with the following events that can be triggered: ...@@ -24,23 +24,24 @@ There, you will see a checkbox with the following events that can be triggered:
- Push - Push
- Issue - Issue
- Confidential issue
- Merge request - Merge request
- Note - Note
- Tag push - Tag push
- Build - Build
- Pipeline
- Wiki page - Wiki page
Bellow each of these event checkboxes, you will have an input field to insert Below each of these event checkboxes, you have an input field to enter
which Mattermost channel you want to send that event message, with `#town-square` which Mattermost channel you want to send that event message. Enter your preferred channel handle (the hash sign `#` is optional).
being the default. The hash sign is optional.
At the end, fill in your Mattermost details: At the end, fill in your Mattermost details:
| Field | Description | | Field | Description |
| ----- | ----------- | | ----- | ----------- |
| **Webhook** | The incoming webhooks which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo... | | **Webhook** | The incoming webhook URL which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo… |
| **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. | | **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. |
| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. | | **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. |
![Mattermost configuration](img/mattermost_configuration.png) ![Mattermost configuration](img/mattermost_configuration.png)
...@@ -21,23 +21,25 @@ There, you will see a checkbox with the following events that can be triggered: ...@@ -21,23 +21,25 @@ There, you will see a checkbox with the following events that can be triggered:
- Push - Push
- Issue - Issue
- Confidential issue
- Merge request - Merge request
- Note - Note
- Tag push - Tag push
- Build - Build
- Pipeline
- Wiki page - Wiki page
Bellow each of these event checkboxes, you will have an input field to insert Below each of these event checkboxes, you have an input field to enter
which Slack channel you want to send that event message, with `#general` which Slack channel you want to send that event message. Enter your preferred channel name **without** the hash sign (`#`).
being the default. Enter your preferred channel **without** the hash sign (`#`).
At the end, fill in your Slack details: At the end, fill in your Slack details:
| Field | Description | | Field | Description |
| ----- | ----------- | | ----- | ----------- |
| **Webhook** | The [incoming webhook URL][slackhook] which you have to setup on Slack. | | **Webhook** | The [incoming webhook URL][slackhook] which you have to setup on Slack. |
| **Username** | Optional username which can be on messages sent to slack. Fill this in if you want to change the username of the bot. | | **Username** | Optional username which can be on messages sent to Slack. Fill this in if you want to change the username of the bot. |
| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. | | **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. |
After you are all done, click **Save changes** for the changes to take effect. After you are all done, click **Save changes** for the changes to take effect.
......
...@@ -63,6 +63,12 @@ git commit -am "Added Debian iso" # commit the file meta data ...@@ -63,6 +63,12 @@ git commit -am "Added Debian iso" # commit the file meta data
git push origin master # sync the git repo and large file to the GitLab server git push origin master # sync the git repo and large file to the GitLab server
``` ```
>**Note**: Make sure that `.gitattributes` is tracked by git. Otherwise Git
LFS will not be working properly for people cloning the project.
```bash
git add .gitattributes
```
Cloning the repository works the same as before. Git automatically detects the Cloning the repository works the same as before. Git automatically detects the
LFS-tracked files and clones them via HTTP. If you performed the git clone LFS-tracked files and clones them via HTTP. If you performed the git clone
command with a SSH URL, you have to enter your GitLab credentials for HTTP command with a SSH URL, you have to enter your GitLab credentials for HTTP
......
...@@ -13,6 +13,7 @@ Feature: Project Commits Branches ...@@ -13,6 +13,7 @@ Feature: Project Commits Branches
Given I visit project protected branches page Given I visit project protected branches page
Then I should see "Shop" protected branches list Then I should see "Shop" protected branches list
@javascript
Scenario: I create a branch Scenario: I create a branch
Given I visit project branches page Given I visit project branches page
And I click new branch link And I click new branch link
...@@ -33,12 +34,7 @@ Feature: Project Commits Branches ...@@ -33,12 +34,7 @@ Feature: Project Commits Branches
And I submit new branch form with invalid name And I submit new branch form with invalid name
Then I should see new an error that branch is invalid Then I should see new an error that branch is invalid
Scenario: I create a branch with invalid reference @javascript
Given I visit project branches page
And I click new branch link
And I submit new branch form with invalid reference
Then I should see new an error that ref is invalid
Scenario: I create a branch that already exists Scenario: I create a branch that already exists
Given I visit project branches page Given I visit project branches page
And I click new branch link And I click new branch link
......
...@@ -34,25 +34,19 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps ...@@ -34,25 +34,19 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
step 'I submit new branch form' do step 'I submit new branch form' do
fill_in 'branch_name', with: 'deploy_keys' fill_in 'branch_name', with: 'deploy_keys'
fill_in 'ref', with: 'master' select_branch('master')
click_button 'Create branch' click_button 'Create branch'
end end
step 'I submit new branch form with invalid name' do step 'I submit new branch form with invalid name' do
fill_in 'branch_name', with: '1.0 stable' fill_in 'branch_name', with: '1.0 stable'
fill_in 'ref', with: 'master' select_branch('master')
click_button 'Create branch'
end
step 'I submit new branch form with invalid reference' do
fill_in 'branch_name', with: 'foo'
fill_in 'ref', with: 'foo'
click_button 'Create branch' click_button 'Create branch'
end end
step 'I submit new branch form with branch that already exists' do step 'I submit new branch form with branch that already exists' do
fill_in 'branch_name', with: 'master' fill_in 'branch_name', with: 'master'
fill_in 'ref', with: 'master' select_branch('master')
click_button 'Create branch' click_button 'Create branch'
end end
...@@ -65,10 +59,6 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps ...@@ -65,10 +59,6 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
expect(page).to have_content "can't contain spaces" expect(page).to have_content "can't contain spaces"
end end
step 'I should see new an error that ref is invalid' do
expect(page).to have_content 'Invalid reference name'
end
step 'I should see new an error that branch already exists' do step 'I should see new an error that branch already exists' do
expect(page).to have_content 'Branch already exists' expect(page).to have_content 'Branch already exists'
end end
...@@ -88,4 +78,12 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps ...@@ -88,4 +78,12 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
step "I should not see branch 'improve/awesome'" do step "I should not see branch 'improve/awesome'" do
expect(page.all(visible: true)).not_to have_content 'improve/awesome' expect(page.all(visible: true)).not_to have_content 'improve/awesome'
end end
def select_branch(branch_name)
click_button 'master'
page.within '#new-branch-form .dropdown-menu' do
click_link branch_name
end
end
end end
...@@ -10,6 +10,7 @@ module API ...@@ -10,6 +10,7 @@ module API
mount ::API::V3::Commits mount ::API::V3::Commits
mount ::API::V3::DeployKeys mount ::API::V3::DeployKeys
mount ::API::V3::Files mount ::API::V3::Files
mount ::API::V3::Groups
mount ::API::V3::Issues mount ::API::V3::Issues
mount ::API::V3::Labels mount ::API::V3::Labels
mount ::API::V3::Members mount ::API::V3::Members
...@@ -90,6 +91,7 @@ module API ...@@ -90,6 +91,7 @@ module API
mount ::API::Projects mount ::API::Projects
mount ::API::ProjectSnippets mount ::API::ProjectSnippets
mount ::API::Repositories mount ::API::Repositories
mount ::API::Runner
mount ::API::Runners mount ::API::Runners
mount ::API::Services mount ::API::Services
mount ::API::Session mount ::API::Session
......
...@@ -52,13 +52,6 @@ module API ...@@ -52,13 +52,6 @@ module API
attrs = declared_params.merge(start_branch: declared_params[:branch], target_branch: declared_params[:branch]) attrs = declared_params.merge(start_branch: declared_params[:branch], target_branch: declared_params[:branch])
attrs[:actions].map! do |action|
action[:action] = action[:action].to_sym
action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/')
action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/')
action
end
result = ::Files::MultiService.new(user_project, current_user, attrs).execute result = ::Files::MultiService.new(user_project, current_user, attrs).execute
if result[:status] == :success if result[:status] == :success
......
...@@ -618,6 +618,10 @@ module API ...@@ -618,6 +618,10 @@ module API
end end
end end
class RunnerRegistrationDetails < Grape::Entity
expose :id, :token
end
class BuildArtifactFile < Grape::Entity class BuildArtifactFile < Grape::Entity
expose :filename, :size expose :filename, :size
end end
......
...@@ -36,12 +36,15 @@ module API ...@@ -36,12 +36,15 @@ module API
optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list' optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list'
optional :all_available, type: Boolean, desc: 'Show all group that you have access to' optional :all_available, type: Boolean, desc: 'Show all group that you have access to'
optional :search, type: String, desc: 'Search for a specific group' optional :search, type: String, desc: 'Search for a specific group'
optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user'
optional :order_by, type: String, values: %w[name path], default: 'name', desc: 'Order by name or path' optional :order_by, type: String, values: %w[name path], default: 'name', desc: 'Order by name or path'
optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)' optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)'
use :pagination use :pagination
end end
get do get do
groups = if current_user.admin groups = if params[:owned]
current_user.owned_groups
elsif current_user.admin
Group.all Group.all
elsif params[:all_available] elsif params[:all_available]
GroupsFinder.new.execute(current_user) GroupsFinder.new.execute(current_user)
...@@ -56,17 +59,6 @@ module API ...@@ -56,17 +59,6 @@ module API
present_groups groups, statistics: params[:statistics] && current_user.is_admin? present_groups groups, statistics: params[:statistics] && current_user.is_admin?
end end
desc 'Get list of owned groups for authenticated user' do
success Entities::Group
end
params do
use :pagination
use :statistics_params
end
get '/owned' do
present_groups current_user.owned_groups, statistics: params[:statistics]
end
desc 'Create a group. Available only for users who can create groups.' do desc 'Create a group. Available only for users who can create groups.' do
success Entities::Group success Entities::Group
end end
......
module API
module Helpers
module Runner
def runner_registration_token_valid?
ActiveSupport::SecurityUtils.variable_size_secure_compare(params[:token],
current_application_settings.runners_registration_token)
end
def get_runner_version_from_params
return unless params['info'].present?
attributes_for_keys(%w(name version revision platform architecture), params['info'])
end
def authenticate_runner!
forbidden! unless current_runner
end
def current_runner
@runner ||= ::Ci::Runner.find_by_token(params[:token].to_s)
end
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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