Commit b83e8a5a authored by Alejandro Rodríguez's avatar Alejandro Rodríguez

Merge branch 'master' into 8-14-stable

parents cb5f3368 6eeff67c
...@@ -213,11 +213,24 @@ rake downtime_check: *exec ...@@ -213,11 +213,24 @@ rake downtime_check: *exec
rake ee_compat_check: rake ee_compat_check:
<<: *exec <<: *exec
only: only:
- branches - branches@gitlab-org/gitlab-ce
- branches@gitlab/gitlabhq
except: except:
- master - master
- tags - tags
- /^[\d-]+-stable(-ee)?$/
allow_failure: yes allow_failure: yes
cache:
key: "ruby231-ee_compat_check_repo"
paths:
- ee_compat_check/repo/
- vendor/ruby
artifacts:
name: "${CI_BUILD_NAME}_${CI_BUILD_REF_NAME}_${CI_BUILD_REF}"
when: on_failure
expire_in: 10d
paths:
- ee_compat_check/patches/*.patch
rake db:migrate:reset: rake db:migrate:reset:
stage: test stage: test
......
...@@ -51,6 +51,7 @@ entry. ...@@ -51,6 +51,7 @@ entry.
- Fail gracefully when creating merge request with non-existing branch (alexsanford) - Fail gracefully when creating merge request with non-existing branch (alexsanford)
- Fix mobile layout issues in admin user overview page !7087 - Fix mobile layout issues in admin user overview page !7087
- Fix HipChat notifications rendering (airatshigapov, eisnerd) - Fix HipChat notifications rendering (airatshigapov, eisnerd)
- Removed unneeded "Builds" and "Environments" link from project titles
- Remove 'Edit' button from wiki edit view !7143 (Hiroyuki Sato) - Remove 'Edit' button from wiki edit view !7143 (Hiroyuki Sato)
- Cleaned up global namespace JS !19661 (Jose Ivan Vargas) - Cleaned up global namespace JS !19661 (Jose Ivan Vargas)
- Refactor Jira service to use jira-ruby gem - Refactor Jira service to use jira-ruby gem
......
...@@ -137,6 +137,7 @@ gem 'acts-as-taggable-on', '~> 4.0' ...@@ -137,6 +137,7 @@ gem 'acts-as-taggable-on', '~> 4.0'
gem 'sidekiq', '~> 4.2' gem 'sidekiq', '~> 4.2'
gem 'sidekiq-cron', '~> 0.4.0' gem 'sidekiq-cron', '~> 0.4.0'
gem 'redis-namespace', '~> 1.5.2' gem 'redis-namespace', '~> 1.5.2'
gem 'sidekiq-limit_fetch', '~> 3.4'
# HTTP requests # HTTP requests
gem 'httparty', '~> 0.13.3' gem 'httparty', '~> 0.13.3'
......
...@@ -685,6 +685,8 @@ GEM ...@@ -685,6 +685,8 @@ GEM
redis-namespace (>= 1.5.2) redis-namespace (>= 1.5.2)
rufus-scheduler (>= 2.0.24) rufus-scheduler (>= 2.0.24)
sidekiq (>= 4.0.0) sidekiq (>= 4.0.0)
sidekiq-limit_fetch (3.4.0)
sidekiq (>= 4)
simplecov (0.12.0) simplecov (0.12.0)
docile (~> 1.1.0) docile (~> 1.1.0)
json (>= 1.8, < 3) json (>= 1.8, < 3)
...@@ -961,6 +963,7 @@ DEPENDENCIES ...@@ -961,6 +963,7 @@ DEPENDENCIES
shoulda-matchers (~> 2.8.0) shoulda-matchers (~> 2.8.0)
sidekiq (~> 4.2) sidekiq (~> 4.2)
sidekiq-cron (~> 0.4.0) sidekiq-cron (~> 0.4.0)
sidekiq-limit_fetch (~> 3.4)
simplecov (= 0.12.0) simplecov (= 0.12.0)
slack-notifier (~> 1.2.0) slack-notifier (~> 1.2.0)
spinach-rails (~> 0.2.1) spinach-rails (~> 0.2.1)
......
...@@ -82,6 +82,10 @@ GitLab is a Ruby on Rails application that runs on the following software: ...@@ -82,6 +82,10 @@ GitLab is a Ruby on Rails application that runs on the following software:
For more information please see the [architecture documentation](https://docs.gitlab.com/ce/development/architecture.html). For more information please see the [architecture documentation](https://docs.gitlab.com/ce/development/architecture.html).
## UX design
Please adhere to the [UX Guide](doc/development/ux_guide/readme.md) when creating designs and implementing code.
## Third-party applications ## Third-party applications
There are a lot of [third-party applications integrating with GitLab](https://about.gitlab.com/applications/). These include GUI Git clients, mobile applications and API wrappers for various languages. There are a lot of [third-party applications integrating with GitLab](https://about.gitlab.com/applications/). These include GUI Git clients, mobile applications and API wrappers for various languages.
......
...@@ -58,11 +58,28 @@ ...@@ -58,11 +58,28 @@
document.addEventListener('page:fetch', gl.utils.cleanupBeforeFetch); document.addEventListener('page:fetch', gl.utils.cleanupBeforeFetch);
window.addEventListener('hashchange', gl.utils.shiftWindow); window.addEventListener('hashchange', gl.utils.shiftWindow);
// automatically adjust scroll position for hash urls taking the height of the navbar into account
// https://github.com/twitter/bootstrap/issues/1768
window.adjustScroll = function() {
var navbar = document.querySelector('.navbar-gitlab');
var subnav = document.querySelector('.layout-nav');
var fixedTabs = document.querySelector('.js-tabs-affix');
adjustment = 0;
if (navbar) adjustment -= navbar.offsetHeight;
if (subnav) adjustment -= subnav.offsetHeight;
if (fixedTabs) adjustment -= fixedTabs.offsetHeight;
return scrollBy(0, adjustment);
};
window.addEventListener("hashchange", adjustScroll);
window.onload = function () { window.onload = function () {
// Scroll the window to avoid the topnav bar // Scroll the window to avoid the topnav bar
// https://github.com/twitter/bootstrap/issues/1768 // https://github.com/twitter/bootstrap/issues/1768
if (location.hash) { if (location.hash) {
return setTimeout(gl.utils.shiftWindow, 100); return setTimeout(adjustScroll, 100);
} }
}; };
......
...@@ -22,6 +22,8 @@ $(() => { ...@@ -22,6 +22,8 @@ $(() => {
gl.IssueBoardsApp.$destroy(true); gl.IssueBoardsApp.$destroy(true);
} }
Store.create();
gl.IssueBoardsApp = new Vue({ gl.IssueBoardsApp = new Vue({
el: $boardApp, el: $boardApp,
components: { components: {
...@@ -37,16 +39,15 @@ $(() => { ...@@ -37,16 +39,15 @@ $(() => {
issueLinkBase: $boardApp.dataset.issueLinkBase, issueLinkBase: $boardApp.dataset.issueLinkBase,
detailIssue: Store.detail detailIssue: Store.detail
}, },
init: Store.create.bind(Store),
computed: { computed: {
detailIssueVisible () { detailIssueVisible () {
return Object.keys(this.detailIssue.issue).length; return Object.keys(this.detailIssue.issue).length;
} },
}, },
created () { created () {
gl.boardService = new BoardService(this.endpoint, this.boardId); gl.boardService = new BoardService(this.endpoint, this.boardId);
}, },
ready () { mounted () {
Store.disabled = this.disabled; Store.disabled = this.disabled;
gl.boardService.all() gl.boardService.all()
.then((resp) => { .then((resp) => {
...@@ -60,6 +61,8 @@ $(() => { ...@@ -60,6 +61,8 @@ $(() => {
} }
}); });
this.state.lists = _.sortBy(this.state.lists, 'position');
Store.addBlankState(); Store.addBlankState();
this.loading = false; this.loading = false;
}); });
...@@ -70,6 +73,9 @@ $(() => { ...@@ -70,6 +73,9 @@ $(() => {
el: '#js-boards-seach', el: '#js-boards-seach',
data: { data: {
filters: Store.state.filters filters: Store.state.filters
},
mounted () {
gl.issueBoards.newListDropdownInit();
} }
}); });
}); });
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
window.gl.issueBoards = window.gl.issueBoards || {}; window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.Board = Vue.extend({ gl.issueBoards.Board = Vue.extend({
template: '#js-board-template',
components: { components: {
'board-list': gl.issueBoards.BoardList, 'board-list': gl.issueBoards.BoardList,
'board-delete': gl.issueBoards.BoardDelete, 'board-delete': gl.issueBoards.BoardDelete,
...@@ -24,7 +25,6 @@ ...@@ -24,7 +25,6 @@
return { return {
detailIssue: Store.detail, detailIssue: Store.detail,
filters: Store.state.filters, filters: Store.state.filters,
showIssueForm: false
}; };
}, },
watch: { watch: {
...@@ -58,10 +58,10 @@ ...@@ -58,10 +58,10 @@
}, },
methods: { methods: {
showNewIssueForm() { showNewIssueForm() {
this.showIssueForm = !this.showIssueForm; this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
} }
}, },
ready () { mounted () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({ const options = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled, disabled: this.disabled,
group: 'boards', group: 'boards',
...@@ -72,13 +72,9 @@ ...@@ -72,13 +72,9 @@
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
const order = this.sortable.toArray(), const order = this.sortable.toArray(),
$board = this.$parent.$refs.board[e.oldIndex + 1], list = Store.findList('id', parseInt(e.item.dataset.id));
list = $board.list;
$board.$destroy(true);
this.$nextTick(() => { this.$nextTick(() => {
Store.state.lists.splice(e.newIndex, 0, list);
Store.moveList(list, order); Store.moveList(list, order);
}); });
} }
...@@ -87,8 +83,5 @@ ...@@ -87,8 +83,5 @@
this.sortable = Sortable.create(this.$el.parentNode, options); this.sortable = Sortable.create(this.$el.parentNode, options);
}, },
beforeDestroy () {
Store.state.lists.$remove(this.list);
}
}); });
})(); })();
...@@ -30,6 +30,8 @@ ...@@ -30,6 +30,8 @@
}); });
}); });
Store.state.lists = _.sortBy(Store.state.lists, 'position');
// Save the labels // Save the labels
gl.boardService.generateDefaultLists() gl.boardService.generateDefaultLists()
.then((resp) => { .then((resp) => {
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
window.gl.issueBoards = window.gl.issueBoards || {}; window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardCard = Vue.extend({ gl.issueBoards.BoardCard = Vue.extend({
template: '#js-board-list-card',
props: { props: {
list: Object, list: Object,
issue: Object, issue: Object,
...@@ -53,11 +54,6 @@ ...@@ -53,11 +54,6 @@
mouseDown () { mouseDown () {
this.showDetail = true; this.showDetail = true;
}, },
mouseMove () {
if (this.showDetail) {
this.showDetail = false;
}
},
showIssue (e) { showIssue (e) {
const targetTagName = e.target.tagName.toLowerCase(); const targetTagName = e.target.tagName.toLowerCase();
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
window.gl.issueBoards = window.gl.issueBoards || {}; window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardList = Vue.extend({ gl.issueBoards.BoardList = Vue.extend({
template: '#js-board-list-template',
components: { components: {
'board-card': gl.issueBoards.BoardCard, 'board-card': gl.issueBoards.BoardCard,
'board-new-issue': gl.issueBoards.BoardNewIssue 'board-new-issue': gl.issueBoards.BoardNewIssue
...@@ -19,20 +20,20 @@ ...@@ -19,20 +20,20 @@
issues: Array, issues: Array,
loading: Boolean, loading: Boolean,
issueLinkBase: String, issueLinkBase: String,
showIssueForm: Boolean
}, },
data () { data () {
return { return {
scrollOffset: 250, scrollOffset: 250,
filters: Store.state.filters, filters: Store.state.filters,
showCount: false showCount: false,
showIssueForm: false
}; };
}, },
watch: { watch: {
filters: { filters: {
handler () { handler () {
this.list.loadingMore = false; this.list.loadingMore = false;
this.$els.list.scrollTop = 0; this.$refs.list.scrollTop = 0;
}, },
deep: true deep: true
}, },
...@@ -51,15 +52,20 @@ ...@@ -51,15 +52,20 @@
}); });
} }
}, },
computed: {
orderedIssues () {
return _.sortBy(this.issues, 'priority');
},
},
methods: { methods: {
listHeight () { listHeight () {
return this.$els.list.getBoundingClientRect().height; return this.$refs.list.getBoundingClientRect().height;
}, },
scrollHeight () { scrollHeight () {
return this.$els.list.scrollHeight; return this.$refs.list.scrollHeight;
}, },
scrollTop () { scrollTop () {
return this.$els.list.scrollTop + this.listHeight(); return this.$refs.list.scrollTop + this.listHeight();
}, },
loadNextPage () { loadNextPage () {
const getIssues = this.list.nextPage(); const getIssues = this.list.nextPage();
...@@ -72,7 +78,7 @@ ...@@ -72,7 +78,7 @@
} }
}, },
}, },
ready () { mounted () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({ const options = gl.issueBoards.getBoardSortableDefaultOptions({
group: 'issues', group: 'issues',
sort: false, sort: false,
...@@ -81,23 +87,27 @@ ...@@ -81,23 +87,27 @@
onStart: (e) => { onStart: (e) => {
const card = this.$refs.issue[e.oldIndex]; const card = this.$refs.issue[e.oldIndex];
card.showDetail = false;
Store.moving.issue = card.issue; Store.moving.issue = card.issue;
Store.moving.list = card.list; Store.moving.list = card.list;
gl.issueBoards.onStart(); gl.issueBoards.onStart();
}, },
onAdd: (e) => { onAdd: (e) => {
gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue); // Add the element back to original list to allow Vue to handle DOM updates
e.from.appendChild(e.item);
this.$nextTick(() => {
// Update the issues once we know the element has been moved
gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue);
});
}, },
onRemove: (e) => {
this.$refs.issue[e.oldIndex].$destroy(true);
}
}); });
this.sortable = Sortable.create(this.$els.list, options); this.sortable = Sortable.create(this.$refs.list, options);
// Scroll event on list to load more // Scroll event on list to load more
this.$els.list.onscroll = () => { this.$refs.list.onscroll = () => {
if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) { if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
this.loadNextPage(); this.loadNextPage();
} }
......
...@@ -7,7 +7,6 @@ ...@@ -7,7 +7,6 @@
gl.issueBoards.BoardNewIssue = Vue.extend({ gl.issueBoards.BoardNewIssue = Vue.extend({
props: { props: {
list: Object, list: Object,
showIssueForm: Boolean
}, },
data() { data() {
return { return {
...@@ -15,11 +14,6 @@ ...@@ -15,11 +14,6 @@
error: false error: false
}; };
}, },
watch: {
showIssueForm () {
this.$els.input.focus();
}
},
methods: { methods: {
submit(e) { submit(e) {
e.preventDefault(); e.preventDefault();
...@@ -37,28 +31,30 @@ ...@@ -37,28 +31,30 @@
this.list.newIssue(issue) this.list.newIssue(issue)
.then((data) => { .then((data) => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions // Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$els.submitButton).enable(); $(this.$refs.submitButton).enable();
Store.detail.issue = issue; Store.detail.issue = issue;
}) })
.catch(() => { .catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions // Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$els.submitButton).enable(); $(this.$refs.submitButton).enable();
// Remove the issue // Remove the issue
this.list.removeIssue(issue); this.list.removeIssue(issue);
// Show error message // Show error message
this.error = true; this.error = true;
this.showIssueForm = true;
}); });
this.cancel(); this.cancel();
}, },
cancel() { cancel() {
this.showIssueForm = false;
this.title = ''; this.title = '';
this.$parent.showIssueForm = false;
} }
} },
mounted() {
this.$refs.input.focus();
},
}); });
})(); })();
...@@ -41,7 +41,7 @@ ...@@ -41,7 +41,7 @@
this.detail.issue = {}; this.detail.issue = {};
} }
}, },
ready () { mounted () {
new IssuableContext(this.currentUser); new IssuableContext(this.currentUser);
new MilestoneSelect(); new MilestoneSelect();
new gl.DueDateSelectors(); new gl.DueDateSelectors();
......
/* eslint-disable */ /* eslint-disable */
$(() => { (() => {
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
$(document).off('created.label').on('created.label', (e, label) => { $(document).off('created.label').on('created.label', (e, label) => {
...@@ -15,54 +18,58 @@ $(() => { ...@@ -15,54 +18,58 @@ $(() => {
}); });
}); });
$('.js-new-board-list').each(function () { gl.issueBoards.newListDropdownInit = () => {
const $this = $(this); $('.js-new-board-list').each(function () {
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path')); const $this = $(this);
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
$this.glDropdown({ $this.glDropdown({
data(term, callback) { data(term, callback) {
$.get($this.attr('data-labels')) $.get($this.attr('data-labels'))
.then((resp) => { .then((resp) => {
callback(resp); callback(resp);
}); });
}, },
renderRow (label) { renderRow (label) {
const active = Store.findList('title', label.title), const active = Store.findList('title', label.title),
$li = $('<li />'), $li = $('<li />'),
$a = $('<a />', { $a = $('<a />', {
class: (active ? `is-active js-board-list-${active.id}` : ''), class: (active ? `is-active js-board-list-${active.id}` : ''),
text: label.title, text: label.title,
href: '#' href: '#'
}), }),
$labelColor = $('<span />', { $labelColor = $('<span />', {
class: 'dropdown-label-box', class: 'dropdown-label-box',
style: `background-color: ${label.color}` style: `background-color: ${label.color}`
}); });
return $li.append($a.prepend($labelColor)); return $li.append($a.prepend($labelColor));
}, },
search: { search: {
fields: ['title'] fields: ['title']
}, },
filterable: true, filterable: true,
selectable: true, selectable: true,
multiSelect: true, multiSelect: true,
clicked (label, $el, e) { clicked (label, $el, e) {
e.preventDefault(); e.preventDefault();
if (!Store.findList('title', label.title)) { if (!Store.findList('title', label.title)) {
Store.new({ Store.new({
title: label.title,
position: Store.state.lists.length - 2,
list_type: 'label',
label: {
id: label.id,
title: label.title, title: label.title,
color: label.color position: Store.state.lists.length - 2,
} list_type: 'label',
}); label: {
id: label.id,
title: label.title,
color: label.color
}
});
Store.state.lists = _.sortBy(Store.state.lists, 'position');
}
} }
} });
}); });
}); };
}); })();
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
fallbackOnBody: true, fallbackOnBody: true,
ghostClass: 'is-ghost', ghostClass: 'is-ghost',
filter: '.board-delete, .btn', filter: '.board-delete, .btn',
delay: gl.issueBoards.touchEnabled ? 100 : 50, delay: gl.issueBoards.touchEnabled ? 100 : 0,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100, scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20, scrollSpeed: 20,
onStart: gl.issueBoards.onStart, onStart: gl.issueBoards.onStart,
......
...@@ -42,7 +42,8 @@ class List { ...@@ -42,7 +42,8 @@ class List {
} }
destroy () { destroy () {
gl.issueBoards.BoardsStore.state.lists.$remove(this); const index = gl.issueBoards.BoardsStore.state.lists.indexOf(this);
gl.issueBoards.BoardsStore.state.lists.splice(index, 1);
gl.issueBoards.BoardsStore.updateNewListDropdown(this.id); gl.issueBoards.BoardsStore.updateNewListDropdown(this.id);
gl.boardService.destroyList(this.id); gl.boardService.destroyList(this.id);
......
...@@ -39,6 +39,8 @@ ...@@ -39,6 +39,8 @@
// Remove any new issues from the backlog // Remove any new issues from the backlog
// as they will be visible in the new list // as they will be visible in the new list
list.issues.forEach(backlogList.removeIssue.bind(backlogList)); list.issues.forEach(backlogList.removeIssue.bind(backlogList));
this.state.lists = _.sortBy(this.state.lists, 'position');
}); });
this.removeBlankState(); this.removeBlankState();
}, },
...@@ -58,6 +60,8 @@ ...@@ -58,6 +60,8 @@
title: 'Welcome to your Issue Board!', title: 'Welcome to your Issue Board!',
position: 0 position: 0
}); });
this.state.lists = _.sortBy(this.state.lists, 'position');
}, },
removeBlankState () { removeBlankState () {
this.removeList('blank'); this.removeList('blank');
......
/* eslint-disable */ /* eslint-disable */
((w) => { (() => {
w.CommentAndResolveBtn = Vue.extend({ const CommentAndResolveBtn = Vue.extend({
props: { props: {
discussionId: String, discussionId: String,
textareaIsEmpty: Boolean },
data() {
return {
textareaIsEmpty: true
}
}, },
computed: { computed: {
discussion: function () { discussion: function () {
...@@ -35,7 +39,7 @@ ...@@ -35,7 +39,7 @@
} }
} }
}, },
ready: function () { mounted: function () {
const $textarea = $(`#new-discussion-note-form-${this.discussionId} .note-textarea`); const $textarea = $(`#new-discussion-note-form-${this.discussionId} .note-textarea`);
this.textareaIsEmpty = $textarea.val() === ''; this.textareaIsEmpty = $textarea.val() === '';
...@@ -47,4 +51,6 @@ ...@@ -47,4 +51,6 @@
$(`#new-discussion-note-form-${this.discussionId} .note-textarea`).off('input.comment-and-resolve-btn'); $(`#new-discussion-note-form-${this.discussionId} .note-textarea`).off('input.comment-and-resolve-btn');
} }
}); });
Vue.component('comment-and-resolve-btn', CommentAndResolveBtn);
})(window); })(window);
/* eslint-disable */ /* eslint-disable */
((w) => { (() => {
w.ResolveBtn = Vue.extend({ const ResolveBtn = Vue.extend({
props: { props: {
noteId: Number, noteId: Number,
discussionId: String, discussionId: String,
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
}, },
methods: { methods: {
updateTooltip: function () { updateTooltip: function () {
$(this.$els.button) $(this.$refs.button)
.tooltip('hide') .tooltip('hide')
.tooltip('fixTitle'); .tooltip('fixTitle');
}, },
...@@ -89,8 +89,8 @@ ...@@ -89,8 +89,8 @@
}); });
} }
}, },
compiled: function () { mounted: function () {
$(this.$els.button).tooltip({ $(this.$refs.button).tooltip({
container: 'body' container: 'body'
}); });
}, },
...@@ -101,4 +101,6 @@ ...@@ -101,4 +101,6 @@
CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy); CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy);
} }
}); });
})(window);
Vue.component('resolve-btn', ResolveBtn);
})();
...@@ -13,6 +13,9 @@ ...@@ -13,6 +13,9 @@
computed: { computed: {
allResolved: function () { allResolved: function () {
return this.resolvedDiscussionCount === this.discussionCount; return this.resolvedDiscussionCount === this.discussionCount;
},
resolvedCountText() {
return this.discussionCount === 1 ? 'discussion' : 'discussions';
} }
} }
}); });
......
/* eslint-disable */ /* eslint-disable */
((w) => { (() => {
w.ResolveDiscussionBtn = Vue.extend({ const ResolveDiscussionBtn = Vue.extend({
props: { props: {
discussionId: String, discussionId: String,
mergeRequestId: Number, mergeRequestId: Number,
...@@ -54,4 +54,6 @@ ...@@ -54,4 +54,6 @@
CommentsStore.createDiscussion(this.discussionId, this.canResolve); CommentsStore.createDiscussion(this.discussionId, this.canResolve);
} }
}); });
})(window);
Vue.component('resolve-discussion-btn', ResolveDiscussionBtn);
})();
...@@ -8,24 +8,35 @@ ...@@ -8,24 +8,35 @@
//= require_directory ./components //= require_directory ./components
$(() => { $(() => {
window.DiffNotesApp = new Vue({ const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn';
el: '#diff-notes-app',
components: { window.gl = window.gl || {};
'resolve-btn': ResolveBtn, window.gl.diffNoteApps = {};
'resolve-discussion-btn': ResolveDiscussionBtn,
'comment-and-resolve-btn': CommentAndResolveBtn gl.diffNotesCompileComponents = () => {
}, const $components = $(COMPONENT_SELECTOR).filter(function () {
methods: { return $(this).closest('resolve-count').length !== 1;
compileComponents: function () { });
const $components = $('resolve-btn, resolve-discussion-btn, jump-to-discussion');
if ($components.length) { if ($components) {
$components.each(function () { $components.each(function () {
DiffNotesApp.$compile($(this).get(0)); const $this = $(this);
}); const noteId = $this.attr(':note-id');
const tmp = Vue.extend({
template: $this.get(0).outerHTML
});
const tmpApp = new tmp().$mount();
if (noteId) {
gl.diffNoteApps[`note_${noteId}`] = tmpApp;
} }
}
$this.replaceWith(tmpApp.$el);
});
} }
}); };
gl.diffNotesCompileComponents();
new Vue({ new Vue({
el: '#resolve-count-app', el: '#resolve-count-app',
......
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
this.loadEditor(); this.loadEditor();
} }
}, },
ready() { mounted() {
if (this.file.loadEditor) { if (this.file.loadEditor) {
this.loadEditor(); this.loadEditor();
} }
......
/* eslint-disable */
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.parallelConflictLine = Vue.extend({
props: {
file: Object,
line: Object
},
mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
template: '#parallel-conflict-line'
});
})(window.gl || (window.gl = {}));
...@@ -7,10 +7,22 @@ ...@@ -7,10 +7,22 @@
props: { props: {
file: Object file: Object
}, },
mixins: [global.mergeConflicts.utils], mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
components: { template: `
'parallel-conflict-line': gl.mergeConflicts.parallelConflictLine <table>
} <tr class="line_holder parallel" v-for="section in file.parallelLines">
<template v-for="line in section">
<td class="diff-line-num header" :class="lineCssClass(line)" v-if="line.isHeader"></td>
<td class="line_content header" :class="lineCssClass(line)" v-if="line.isHeader">
<strong>{{line.richText}}</strong>
<button class="btn" @click="handleSelected(file, line.id, line.section)">{{line.buttonTitle}}</button>
</td>
<td class="diff-line-num old_line" :class="lineCssClass(line)" v-if="!line.isHeader">{{line.lineNumber}}</td>
<td class="line_content parallel" :class="lineCssClass(line)" v-if="!line.isHeader" v-html="line.richText"></td>
</template>
</tr>
</table>
`,
}); });
})(window.gl || (window.gl = {})); })(window.gl || (window.gl = {}));
...@@ -6,7 +6,6 @@ ...@@ -6,7 +6,6 @@
//= require ./mixins/line_conflict_actions //= require ./mixins/line_conflict_actions
//= require ./components/diff_file_editor //= require ./components/diff_file_editor
//= require ./components/inline_conflict_lines //= require ./components/inline_conflict_lines
//= require ./components/parallel_conflict_line
//= require ./components/parallel_conflict_lines //= require ./components/parallel_conflict_lines
$(() => { $(() => {
...@@ -49,7 +48,7 @@ $(() => { ...@@ -49,7 +48,7 @@ $(() => {
mergeConflictsStore.setLoadingState(false); mergeConflictsStore.setLoadingState(false);
this.$nextTick(() => { this.$nextTick(() => {
$(conflictsEl.querySelectorAll('.js-syntax-highlight')).syntaxHighlight(); $('.js-syntax-highlight').syntaxHighlight();
}); });
}); });
}, },
......
...@@ -130,7 +130,7 @@ ...@@ -130,7 +130,7 @@
MergeRequestTabs.prototype.scrollToElement = function(container) { MergeRequestTabs.prototype.scrollToElement = function(container) {
var $el, navBarHeight; var $el, navBarHeight;
if (window.location.hash) { if (window.location.hash) {
navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight(); navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + document.querySelector('.js-tabs-affix').offsetHeight;
$el = $(container + " " + window.location.hash + ":not(.match)"); $el = $(container + " " + window.location.hash + ":not(.match)");
if ($el.length) { if ($el.length) {
return $.scrollTo(container + " " + window.location.hash + ":not(.match)", { return $.scrollTo(container + " " + window.location.hash + ":not(.match)", {
...@@ -227,8 +227,8 @@ ...@@ -227,8 +227,8 @@
return function(data) { return function(data) {
$('#diffs').html(data.html); $('#diffs').html(data.html);
if (typeof DiffNotesApp !== 'undefined') { if (typeof gl.diffNotesCompileComponents !== 'undefined') {
DiffNotesApp.compileComponents(); gl.diffNotesCompileComponents();
} }
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
......
...@@ -325,8 +325,8 @@ ...@@ -325,8 +325,8 @@
discussionContainer.append(note_html); discussionContainer.append(note_html);
} }
if (typeof DiffNotesApp !== 'undefined') { if (typeof gl.diffNotesCompileComponents !== 'undefined') {
DiffNotesApp.compileComponents(); gl.diffNotesCompileComponents();
} }
gl.utils.localTimeAgo($('.js-timeago', note_html), false); gl.utils.localTimeAgo($('.js-timeago', note_html), false);
...@@ -466,8 +466,8 @@ ...@@ -466,8 +466,8 @@
$note_li.replaceWith($html); $note_li.replaceWith($html);
if (typeof DiffNotesApp !== 'undefined') { if (typeof gl.diffNotesCompileComponents !== 'undefined') {
DiffNotesApp.compileComponents(); gl.diffNotesCompileComponents();
} }
}; };
...@@ -559,11 +559,9 @@ ...@@ -559,11 +559,9 @@
note = $(el); note = $(el);
notes = note.closest(".notes"); notes = note.closest(".notes");
if (typeof DiffNotesApp !== "undefined" && DiffNotesApp !== null) { if (typeof gl.diffNotesCompileComponents !== 'undefined') {
ref = DiffNotesApp.$refs[noteId]; if (gl.diffNoteApps[noteId]) {
gl.diffNoteApps[noteId].$destroy();
if (ref) {
ref.$destroy(true);
} }
} }
...@@ -643,11 +641,12 @@ ...@@ -643,11 +641,12 @@
form.find('.js-note-target-close').remove(); form.find('.js-note-target-close').remove();
this.setupNoteForm(form); this.setupNoteForm(form);
if (typeof DiffNotesApp !== 'undefined') { if (typeof gl.diffNotesCompileComponents !== 'undefined') {
var $commentBtn = form.find('comment-and-resolve-btn'); var $commentBtn = form.find('comment-and-resolve-btn');
$commentBtn $commentBtn
.attr(':discussion-id', "'" + dataHolder.data('discussionId') + "'"); .attr(':discussion-id', "'" + dataHolder.data('discussionId') + "'");
DiffNotesApp.$compile($commentBtn.get(0));
gl.diffNotesCompileComponents();
} }
form.find(".js-note-text").focus(); form.find(".js-note-text").focus();
......
...@@ -45,15 +45,15 @@ ...@@ -45,15 +45,15 @@
this.content.hide(); this.content.hide();
this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down'); this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down');
this.collapsedContent.show(); this.collapsedContent.show();
if (typeof DiffNotesApp !== 'undefined') { if (typeof gl.diffNotesCompileComponents !== 'undefined') {
DiffNotesApp.compileComponents(); gl.diffNotesCompileComponents();
} }
} else if (this.content) { } else if (this.content) {
this.collapsedContent.hide(); this.collapsedContent.hide();
this.content.show(); this.content.show();
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right'); this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
if (typeof DiffNotesApp !== 'undefined') { if (typeof gl.diffNotesCompileComponents !== 'undefined') {
DiffNotesApp.compileComponents(); gl.diffNotesCompileComponents();
} }
} else { } else {
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right'); this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
...@@ -76,8 +76,8 @@ ...@@ -76,8 +76,8 @@
} }
_this.collapsedContent.after(_this.content); _this.collapsedContent.after(_this.content);
if (typeof DiffNotesApp !== 'undefined') { if (typeof gl.diffNotesCompileComponents !== 'undefined') {
DiffNotesApp.compileComponents(); gl.diffNotesCompileComponents();
} }
if (cb) cb(); if (cb) cb();
......
...@@ -71,7 +71,7 @@ ...@@ -71,7 +71,7 @@
display: none; display: none;
} }
.group-right-buttons { .group-buttons {
display: none; display: none;
} }
......
...@@ -166,8 +166,12 @@ ...@@ -166,8 +166,12 @@
} }
} }
.board-list { .board-list-component {
height: calc(100% - 49px); height: calc(100% - 49px);
}
.board-list {
height: 100%;
margin-bottom: 0; margin-bottom: 0;
padding: 5px; padding: 5px;
list-style: none; list-style: none;
...@@ -175,7 +179,7 @@ ...@@ -175,7 +179,7 @@
overflow-x: hidden; overflow-x: hidden;
&.is-smaller { &.is-smaller {
height: calc(100% - 185px); height: calc(100% - 136px);
} }
} }
......
...@@ -62,6 +62,8 @@ ...@@ -62,6 +62,8 @@
.ci-status-link { .ci-status-link {
display: inline-block; display: inline-block;
position: relative;
top: 1px;
} }
.btn-clipboard, .btn-clipboard,
......
...@@ -10,7 +10,6 @@ ...@@ -10,7 +10,6 @@
} }
.group-row { .group-row {
.stats { .stats {
float: right; float: right;
line-height: $list-text-height; line-height: $list-text-height;
...@@ -23,36 +22,18 @@ ...@@ -23,36 +22,18 @@
} }
.ldap-group-links { .ldap-group-links {
.form-actions { .form-actions {
margin-bottom: $gl-padding; margin-bottom: $gl-padding;
} }
} }
.groups-cover-block { .group-buttons {
.notification-dropdown {
.container-fluid { display: inline-block;
position: relative;
}
.group-right-buttons {
position: absolute;
right: 16px;
.btn {
@include btn-gray;
padding: 3px 10px;
background-color: $background-color;
}
}
.group-avatar {
border: 0;
} }
} }
.groups-header { .groups-header {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
.nav-links { .nav-links {
width: 35%; width: 35%;
......
...@@ -267,20 +267,6 @@ ...@@ -267,20 +267,6 @@
} }
} }
.issuable-header-btn {
background: $gray-normal;
border: 1px solid $border-gray-normal;
&:hover {
background: $gray-dark;
border: 1px solid $border-gray-dark;
}
&.btn-primary {
@extend .btn-primary;
}
}
a { a {
&:hover { &:hover {
color: $md-link-color; color: $md-link-color;
......
...@@ -47,6 +47,7 @@ ...@@ -47,6 +47,7 @@
&.right { &.right {
float: right; float: right;
padding-right: 0;
a { a {
color: $gl-gray; color: $gl-gray;
......
...@@ -86,7 +86,8 @@ ...@@ -86,7 +86,8 @@
} }
} }
.project-home-panel { .project-home-panel,
.group-home-panel {
padding-top: 24px; padding-top: 24px;
padding-bottom: 24px; padding-bottom: 24px;
...@@ -94,7 +95,8 @@ ...@@ -94,7 +95,8 @@
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
} }
.project-avatar { .project-avatar,
.group-avatar {
float: none; float: none;
margin: 0 auto; margin: 0 auto;
border: none; border: none;
...@@ -104,7 +106,8 @@ ...@@ -104,7 +106,8 @@
} }
} }
.project-title { .project-title,
.group-title {
margin-top: 10px; margin-top: 10px;
margin-bottom: 10px; margin-bottom: 10px;
font-size: 24px; font-size: 24px;
...@@ -118,10 +121,11 @@ ...@@ -118,10 +121,11 @@
} }
} }
.project-home-desc { .project-home-desc,
.group-home-desc {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
margin-bottom: 15px; margin-bottom: 0;
max-width: 700px; max-width: 700px;
> p { > p {
...@@ -141,13 +145,18 @@ ...@@ -141,13 +145,18 @@
} }
} }
.project-repo-buttons { .project-repo-buttons,
font-size: 0; .group-buttons {
margin-top: 15px;
.btn { .btn {
@include btn-gray; @include btn-gray;
padding: 3px 10px; padding: 3px 10px;
&:last-child {
margin-left: 0;
}
.fa { .fa {
color: $layout-link-gray; color: $layout-link-gray;
} }
...@@ -168,7 +177,8 @@ ...@@ -168,7 +177,8 @@
} }
} }
.project-repo-btn-group, .download-button,
.dropdown-toggle,
.notification-dropdown, .notification-dropdown,
.project-dropdown { .project-dropdown {
margin-left: 10px; margin-left: 10px;
...@@ -474,9 +484,7 @@ a.deploy-project-label { ...@@ -474,9 +484,7 @@ a.deploy-project-label {
margin-right: $gl-padding; margin-right: $gl-padding;
} }
&.project-repo-buttons-right { &.right {
margin-top: 10px;
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
float: right; float: right;
margin-top: 0; margin-top: 0;
......
...@@ -117,6 +117,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -117,6 +117,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:send_user_confirmation_email, :send_user_confirmation_email,
:container_registry_token_expire_delay, :container_registry_token_expire_delay,
:enabled_git_access_protocol, :enabled_git_access_protocol,
:sidekiq_throttling_enabled,
:sidekiq_throttling_factor,
:housekeeping_enabled, :housekeeping_enabled,
:housekeeping_bitmaps_enabled, :housekeeping_bitmaps_enabled,
:housekeeping_incremental_repack_period, :housekeeping_incremental_repack_period,
...@@ -125,7 +127,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -125,7 +127,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
repository_storages: [], repository_storages: [],
restricted_visibility_levels: [], restricted_visibility_levels: [],
import_sources: [], import_sources: [],
disabled_oauth_sign_in_sources: [] disabled_oauth_sign_in_sources: [],
sidekiq_throttling_queues: []
) )
end end
end end
...@@ -3,7 +3,7 @@ module DiffForPath ...@@ -3,7 +3,7 @@ module DiffForPath
def render_diff_for_path(diffs) def render_diff_for_path(diffs)
diff_file = diffs.diff_files.find do |diff| diff_file = diffs.diff_files.find do |diff|
diff.old_path == params[:old_path] && diff.new_path == params[:new_path] diff.file_identifier == params[:file_identifier]
end end
return render_404 unless diff_file return render_404 unless diff_file
......
...@@ -58,7 +58,7 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -58,7 +58,7 @@ class Groups::MilestonesController < Groups::ApplicationController
def render_new_with_error(empty_project_ids) def render_new_with_error(empty_project_ids)
@milestone = Milestone.new(milestone_params) @milestone = Milestone.new(milestone_params)
@milestone.errors.add(:project_id, "Please select at least one project.") if empty_project_ids @milestone.errors.add(:base, "Please select at least one project.") if empty_project_ids
render :new render :new
end end
......
...@@ -507,6 +507,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -507,6 +507,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.close @merge_request.close
end end
labels
define_pipelines_vars define_pipelines_vars
end end
......
...@@ -18,7 +18,9 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -18,7 +18,9 @@ class Projects::PipelinesController < Projects::ApplicationController
end end
def create def create
@pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute(ignore_skip_ci: true, save_on_errors: false) @pipeline = Ci::CreatePipelineService
.new(project, current_user, create_params)
.execute(ignore_skip_ci: true, save_on_errors: false)
unless @pipeline.persisted? unless @pipeline.persisted?
render 'new' render 'new'
return return
......
...@@ -100,4 +100,8 @@ module ApplicationSettingsHelper ...@@ -100,4 +100,8 @@ module ApplicationSettingsHelper
options_for_select(options, @application_setting.repository_storages) options_for_select(options, @application_setting.repository_storages)
end end
def sidekiq_queue_options_for_select
options_for_select(Sidekiq::Queue.all.map(&:name), @application_setting.sidekiq_throttling_queues)
end
end end
...@@ -49,7 +49,7 @@ module ProjectsHelper ...@@ -49,7 +49,7 @@ module ProjectsHelper
end end
end end
def project_title(project, name = nil, url = nil) def project_title(project)
namespace_link = namespace_link =
if project.group if project.group
link_to(simple_sanitize(project.group.name), group_path(project.group)) link_to(simple_sanitize(project.group.name), group_path(project.group))
...@@ -66,10 +66,7 @@ module ProjectsHelper ...@@ -66,10 +66,7 @@ module ProjectsHelper
end end
end end
full_title = "#{namespace_link} / #{project_link}".html_safe "#{namespace_link} / #{project_link}".html_safe
full_title << ' &middot; '.html_safe << link_to(simple_sanitize(name), url) if name
full_title
end end
def remove_project_message(project) def remove_project_message(project)
......
...@@ -19,6 +19,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -19,6 +19,7 @@ class ApplicationSetting < ActiveRecord::Base
serialize :domain_whitelist, Array serialize :domain_whitelist, Array
serialize :domain_blacklist, Array serialize :domain_blacklist, Array
serialize :repository_storages serialize :repository_storages
serialize :sidekiq_throttling_queues, Array
cache_markdown_field :sign_in_text cache_markdown_field :sign_in_text
cache_markdown_field :help_page_text cache_markdown_field :help_page_text
...@@ -85,6 +86,15 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -85,6 +86,15 @@ class ApplicationSetting < ActiveRecord::Base
presence: { message: 'Domain blacklist cannot be empty if Blacklist is enabled.' }, presence: { message: 'Domain blacklist cannot be empty if Blacklist is enabled.' },
if: :domain_blacklist_enabled? if: :domain_blacklist_enabled?
validates :sidekiq_throttling_factor,
numericality: { greater_than: 0, less_than: 1 },
presence: { message: 'Throttling factor cannot be empty if Sidekiq Throttling is enabled.' },
if: :sidekiq_throttling_enabled?
validates :sidekiq_throttling_queues,
presence: { message: 'Queues to throttle cannot be empty if Sidekiq Throttling is enabled.' },
if: :sidekiq_throttling_enabled?
validates :housekeeping_incremental_repack_period, validates :housekeeping_incremental_repack_period,
presence: true, presence: true,
numericality: { only_integer: true, greater_than: 0 } numericality: { only_integer: true, greater_than: 0 }
...@@ -180,6 +190,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -180,6 +190,7 @@ class ApplicationSetting < ActiveRecord::Base
container_registry_token_expire_delay: 5, container_registry_token_expire_delay: 5,
repository_storages: ['default'], repository_storages: ['default'],
user_default_external: false, user_default_external: false,
sidekiq_throttling_enabled: false,
housekeeping_enabled: true, housekeeping_enabled: true,
housekeeping_bitmaps_enabled: true, housekeeping_bitmaps_enabled: true,
housekeeping_incremental_repack_period: 10, housekeeping_incremental_repack_period: 10,
......
...@@ -1334,6 +1334,10 @@ class Project < ActiveRecord::Base ...@@ -1334,6 +1334,10 @@ class Project < ActiveRecord::Base
end end
end end
def only_allow_merge_if_all_discussions_are_resolved
super || false
end
private private
def pushes_since_gc_redis_key def pushes_since_gc_redis_key
......
...@@ -84,15 +84,17 @@ class Repository ...@@ -84,15 +84,17 @@ class Repository
def commit(ref = 'HEAD') def commit(ref = 'HEAD')
return nil unless exists? return nil unless exists?
commit = commit =
if ref.is_a?(Gitlab::Git::Commit) if ref.is_a?(Gitlab::Git::Commit)
ref ref
else else
Gitlab::Git::Commit.find(raw_repository, ref) Gitlab::Git::Commit.find(raw_repository, ref)
end end
commit = ::Commit.new(commit, @project) if commit commit = ::Commit.new(commit, @project) if commit
commit commit
rescue Rugged::OdbError rescue Rugged::OdbError, Rugged::TreeError
nil nil
end end
...@@ -232,6 +234,8 @@ class Repository ...@@ -232,6 +234,8 @@ class Repository
def ref_exists?(ref) def ref_exists?(ref)
rugged.references.exist?(ref) rugged.references.exist?(ref)
rescue Rugged::ReferenceError
false
end end
def update_ref!(name, newrev, oldrev) def update_ref!(name, newrev, oldrev)
...@@ -239,7 +243,7 @@ class Repository ...@@ -239,7 +243,7 @@ class Repository
# offer 'compare and swap' ref updates. Without compare-and-swap we can # offer 'compare and swap' ref updates. Without compare-and-swap we can
# (and have!) accidentally reset the ref to an earlier state, clobbering # (and have!) accidentally reset the ref to an earlier state, clobbering
# commits. See also https://github.com/libgit2/libgit2/issues/1534. # commits. See also https://github.com/libgit2/libgit2/issues/1534.
command = %w[git update-ref --stdin -z] command = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z)
_, status = Gitlab::Popen.popen(command, path_to_repo) do |stdin| _, status = Gitlab::Popen.popen(command, path_to_repo) do |stdin|
stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00") stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00")
end end
...@@ -270,11 +274,7 @@ class Repository ...@@ -270,11 +274,7 @@ class Repository
end end
def kept_around?(sha) def kept_around?(sha)
begin ref_exists?(keep_around_ref_name(sha))
ref_exists?(keep_around_ref_name(sha))
rescue Rugged::ReferenceError
false
end
end end
def tag_names def tag_names
......
...@@ -283,6 +283,31 @@ ...@@ -283,6 +283,31 @@
The amount of points to store in a single UDP packet. More points The amount of points to store in a single UDP packet. More points
results in fewer but larger UDP packets being sent. results in fewer but larger UDP packets being sent.
%fieldset
%legend Background Jobs
%p
These settings require a restart to take effect.
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :sidekiq_throttling_enabled do
= f.check_box :sidekiq_throttling_enabled
Enable Sidekiq Job Throttling
.help-block
Limit the amount of resources slow running jobs are assigned.
.form-group
= f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'control-label col-sm-2'
.col-sm-10
= f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' }
.help-block
Choose which queues you wish to throttle.
.form-group
= f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01'
.help-block
The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.
%fieldset %fieldset
%legend Spam and Anti-bot Protection %legend Spam and Anti-bot Protection
.form-group .form-group
......
...@@ -8,3 +8,6 @@ ...@@ -8,3 +8,6 @@
- if signin_enabled? - if signin_enabled?
%li %li
= link_to 'Standard', '#ldap-standard', 'data-toggle' => 'tab' = link_to 'Standard', '#ldap-standard', 'data-toggle' => 'tab'
- if signin_enabled? && signup_enabled?
%li
= link_to 'Register', '#register-pane', 'data-toggle' => 'tab'
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
= f.label :projects, "Projects", class: "control-label" = f.label :projects, "Projects", class: "control-label"
.col-sm-10 .col-sm-10
= f.collection_select :project_ids, @group.projects.non_archived, :id, :name, = f.collection_select :project_ids, @group.projects.non_archived, :id, :name,
{ selected: @group.projects.non_archived.pluck(:id) }, multiple: true, class: 'select2' { selected: @group.projects.non_archived.pluck(:id) }, required: true, multiple: true, class: 'select2'
.col-md-6 .col-md-6
.form-group .form-group
......
...@@ -4,25 +4,23 @@ ...@@ -4,25 +4,23 @@
- if current_user - if current_user
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity") = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
.cover-block.groups-cover-block .group-home-panel.text-center
%div{ class: container_class } %div{ class: container_class }
.avatar-container.s70.group-avatar .avatar-container.s70.group-avatar
= image_tag group_icon(@group), class: "avatar s70 avatar-tile" = image_tag group_icon(@group), class: "avatar s70 avatar-tile"
.group-info %h1.group-title
.cover-title @#{@group.path}
%h1 %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
@#{@group.path} = visibility_level_icon(@group.visibility_level, fw: false)
%span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
= visibility_level_icon(@group.visibility_level, fw: false)
.group-right-buttons.btn-group - if @group.description.present?
- if current_user .group-home-desc
.pull-left.append-right-10= render 'shared/members/access_request_buttons', source: @group = markdown_field(@group, :description)
= render 'shared/notifications/button', notification_setting: @notification_setting
- if @group.description.present? - if current_user
.cover-desc.description .group-buttons
= markdown_field(@group, :description) = render 'shared/members/access_request_buttons', source: @group
= render 'shared/notifications/button', notification_setting: @notification_setting
%div.groups-header{ class: container_class } %div.groups-header{ class: container_class }
.top-area .top-area
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
- if can_admin_group - if can_admin_group
= nav_link(path: 'groups#projects') do = nav_link(path: 'groups#projects') do
= link_to 'Projects', projects_group_path(@group), title: 'Projects' = link_to 'Projects', projects_group_path(@group), title: 'Projects'
- if can_edit || can_leave - if (can_edit || can_leave) && can_admin_group
%li.divider %li.divider
- if can_edit - if can_edit
%li %li
......
- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds' - page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds'
- header_title project_title(@project, "Builds", project_builds_path(@project))
.top-block.row-content-block.clearfix .top-block.row-content-block.clearfix
.pull-right .pull-right
......
- @no_container = true
- @content_class = "issue-boards-content"
- page_title "Boards"
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('boards/boards_bundle.js')
= page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
%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-card{ type: "text/x-template" }= render "projects/boards/components/card"
= render "projects/issues/head"
= render 'shared/issuable/filter', type: :boards
#board-app.boards-app{ "v-cloak" => true, data: board_data }
.boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
.boards-app-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin")
%board{ "v-cloak" => true,
"v-for" => "list in state.lists",
"ref" => "board",
":list" => "list",
":disabled" => "disabled",
":issue-link-base" => "issueLinkBase",
":key" => "_uid" }
= render "projects/boards/components/sidebar"
%board-blank-state{ "inline-template" => true, %board-blank-state{ "inline-template" => true,
"v-if" => "list.id == 'blank'" } "v-if" => 'list.id == "blank"' }
.board-blank-state .board-blank-state
%p %p
Add the following default lists to your Issue Board with one click: Add the following default lists to your Issue Board with one click:
......
%board{ "inline-template" => true, .board{ ":class" => '{ "is-draggable": !list.preset }',
"v-cloak" => true, ":data-id" => "list.id" }
"v-for" => "list in state.lists | orderBy 'position'", .board-inner
"v-ref:board" => true, %header.board-header{ ":class" => '{ "has-border": list.label }', ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
":list" => "list", %h3.board-title.js-board-handle{ ":class" => '{ "user-can-drag": (!disabled && !list.preset) }' }
":disabled" => "disabled", %span.has-tooltip{ ":title" => '(list.label ? list.label.description : "")',
":issue-link-base" => "issueLinkBase", data: { container: "body", placement: "bottom" } }
"track-by" => "_uid" } {{ list.title }}
.board{ ":class" => "{ 'is-draggable': !list.preset }", .board-issue-count-holder.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' }
":data-id" => "list.id" } %span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "done" }' }
.board-inner {{ list.issuesSize }}
%header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" } - if can?(current_user, :admin_issue, @project)
%h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" } %button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button",
%span.has-tooltip{ ":title" => "(list.label ? list.label.description : '')", "@click" => "showNewIssueForm",
data: { container: "body", placement: "bottom" } } "v-if" => 'list.type !== "done"',
{{ list.title }} "aria-label" => "Add an issue",
.board-issue-count-holder.pull-right.clearfix{ "v-if" => "list.type !== 'blank'" } "title" => "Add an issue",
%span.board-issue-count.pull-left{ ":class" => "{ 'has-btn': list.type !== 'done' }" } data: { placement: "top", container: "body" } }
{{ list.issuesSize }} = icon("plus")
- if can?(current_user, :admin_issue, @project) - if can?(current_user, :admin_list, @project)
%button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button", %board-delete{ "inline-template" => true,
"@click" => "showNewIssueForm",
"v-if" => "list.type !== 'done'",
"aria-label" => "Add an issue",
"title" => "Add an issue",
data: { placement: "top", container: "body" } }
= icon("plus")
- if can?(current_user, :admin_list, @project)
%board-delete{ "inline-template" => true,
":list" => "list",
"v-if" => "!list.preset && list.id" }
%button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
= icon("trash")
%board-list{ "inline-template" => true,
"v-if" => "list.type !== 'blank'",
":list" => "list",
":issues" => "list.issues",
":loading" => "list.loading",
":disabled" => "disabled",
":show-issue-form.sync" => "showIssueForm",
":issue-link-base" => "issueLinkBase" }
.board-list-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin")
- if can? current_user, :create_issue, @project
%board-new-issue{ "inline-template" => true,
":list" => "list", ":list" => "list",
":show-issue-form.sync" => "showIssueForm", "v-if" => "!list.preset && list.id" }
"v-show" => "list.type !== 'done' && showIssueForm" } %button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
.card.board-new-issue-form = icon("trash")
%form{ "@submit" => "submit($event)" } %board-list{ "v-if" => 'list.type !== "blank"',
.flash-container{ "v-if" => "error" } ":list" => "list",
.flash-alert ":issues" => "list.issues",
An error occured. Please try again. ":loading" => "list.loading",
%label.label-light{ ":for" => "list.id + '-title'" } ":disabled" => "disabled",
Title ":issue-link-base" => "issueLinkBase",
%input.form-control{ type: "text", "ref" => "board-list" }
"v-model" => "title", - if can?(current_user, :admin_list, @project)
"v-el:input" => true, = render "projects/boards/components/blank_state"
":id" => "list.id + '-title'" }
.clearfix.prepend-top-10
%button.btn.btn-success.pull-left{ type: "submit",
":disabled" => "title === ''",
"v-el:submit-button" => true }
Submit issue
%button.btn.btn-default.pull-right{ type: "button",
"@click" => "cancel" }
Cancel
%ul.board-list{ "v-el:list" => true,
"v-show" => "!loading",
":data-board" => "list.id",
":class" => "{ 'is-smaller': showIssueForm }" }
= render "projects/boards/components/card"
%li.board-list-count.text-center{ "v-if" => "showCount" }
= icon("spinner spin", "v-show" => "list.loadingMore" )
%span{ "v-if" => "list.issues.length === list.issuesSize" }
Showing all issues
%span{ "v-else" => true }
Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
- if can?(current_user, :admin_list, @project)
= render "projects/boards/components/blank_state"
.board-list-component
.board-list-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin")
- if can? current_user, :create_issue, @project
%board-new-issue{ "inline-template" => true,
":list" => "list",
"v-if" => 'list.type !== "done" && showIssueForm' }
.card.board-new-issue-form
%form{ "@submit" => "submit($event)" }
.flash-container{ "v-if" => "error" }
.flash-alert
An error occured. Please try again.
%label.label-light{ ":for" => 'list.id + "-title"' }
Title
%input.form-control{ type: "text",
"v-model" => "title",
"ref" => "input",
":id" => 'list.id + "-title"' }
.clearfix.prepend-top-10
%button.btn.btn-success.pull-left{ type: "submit",
":disabled" => 'title === ""',
"ref" => "submit-button" }
Submit issue
%button.btn.btn-default.pull-right{ type: "button",
"@click" => "cancel" }
Cancel
%ul.board-list{ "ref" => "list",
"v-show" => "!loading",
":data-board" => "list.id",
":class" => '{ "is-smaller": showIssueForm }' }
%board-card{ "v-for" => "(issue, index) in orderedIssues",
"ref" => "issue",
":index" => "index",
":list" => "list",
":issue" => "issue",
":issue-link-base" => "issueLinkBase",
":disabled" => "disabled",
"key" => "id" }
%li.board-list-count.text-center{ "v-if" => "showCount" }
= icon("spinner spin", "v-show" => "list.loadingMore" )
%span{ "v-if" => "list.issues.length === list.issuesSize" }
Showing all issues
%span{ "v-else" => true }
Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
%board-card{ "inline-template" => true, %li.card{ ":class" => '{ "user-can-drag": !disabled && issue.id, "is-disabled": disabled || !issue.id, "is-active": issueDetailVisible }',
"v-for" => "issue in issues | orderBy 'priority'", ":index" => "index",
"v-ref:issue" => true, "@mousedown" => "mouseDown",
":index" => "$index", "@mouseup" => "showIssue($event)" }
":list" => "list", %h4.card-title
":issue" => "issue", = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
":issue-link-base" => "issueLinkBase", %a{ ":href" => 'issueLinkBase + "/" + issue.id',
":disabled" => "disabled", ":title" => "issue.title" }
"track-by" => "id" } {{ issue.title }}
%li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }", .card-footer
":index" => "index", %span.card-number{ "v-if" => "issue.id" }
"@mousedown" => "mouseDown", = precede '#' do
"@mouseMove" => "mouseMove", {{ issue.id }}
"@mouseup" => "showIssue($event)" } %a.has-tooltip{ ":href" => "\"#{root_path}\" + issue.assignee.username",
%h4.card-title ":title" => '"Assigned to " + issue.assignee.name',
= icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential") "v-if" => "issue.assignee",
%a{ ":href" => "issueLinkBase + '/' + issue.id", data: { container: 'body' } }
":title" => "issue.title" } %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 }
{{ issue.title }} %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
.card-footer type: "button",
%span.card-number{ "v-if" => "issue.id" } "v-if" => "(!list.label || label.id !== list.label.id)",
= precede '#' do "@click" => "filterByLabel(label, $event)",
{{ issue.id }} ":style" => "{ backgroundColor: label.color, color: label.textColor }",
%a.has-tooltip{ ":href" => "'#{root_path}' + issue.assignee.username", ":title" => "label.description",
":title" => "'Assigned to ' + issue.assignee.name", data: { container: 'body' } }
"v-if" => "issue.assignee", {{ label.title }}
data: { container: 'body' } }
%img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 }
%button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
type: "button",
"v-if" => "(!list.label || label.id !== list.label.id)",
"@click" => "filterByLabel(label, $event)",
":style" => "{ backgroundColor: label.color, color: label.textColor }",
":title" => "label.description",
data: { container: 'body' } }
{{ label.title }}
- @no_container = true = render "show"
- @content_class = "issue-boards-content"
- page_title "Boards"
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('boards/boards_bundle.js')
= page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
= render "projects/issues/head"
= render 'shared/issuable/filter', type: :boards
#board-app.boards-app{ "v-cloak" => true, data: board_data }
.boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
.boards-app-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin")
= render "projects/boards/components/board"
= render "projects/boards/components/sidebar"
- @no_container = true = render "show"
- @content_class = "issue-boards-content"
- page_title "Boards"
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('boards/boards_bundle.js')
= page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
= render "projects/issues/head"
= render 'shared/issuable/filter', type: :boards
#board-app.boards-app{ "v-cloak" => true, data: board_data }
.boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
.boards-app-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin")
= render "projects/boards/components/board"
= render "projects/boards/components/sidebar"
- @no_container = true - @no_container = true
- page_title "#{@build.name} (##{@build.id})", "Builds" - page_title "#{@build.name} (##{@build.id})", "Builds"
- header_title project_title(@project, "Builds", project_builds_path(@project)) - trace_with_state = @build.trace_with_state
= render "projects/pipelines/head", build_subnav: true = render "projects/pipelines/head", build_subnav: true
%div{ class: container_class } %div{ class: container_class }
......
- if current_user - if current_user
.dropdown.inline.project-dropdown .dropdown.inline
%a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"} %a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
= icon('plus') = icon('plus')
= icon("caret-down") = icon("caret-down")
......
.pipeline-graph-container .pipeline-graph-container
.row-content-block.build-content.middle-block.pipeline-actions .row-content-block.build-content.middle-block.pipeline-actions
.pull-right .pull-right
.btn.btn-grouped.btn-white.toggle-pipeline-btn %button.btn.btn-grouped.btn-white.toggle-pipeline-btn
%span.toggle-btn-text Hide %span.toggle-btn-text Hide
%span pipeline graph %span pipeline graph
%span.caret %span.caret
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
- note_count = notes.user.count - note_count = notes.user.count
- cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count] - cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count]
- cache_key.push(commit.status) if commit.status - cache_key.push(commit.status(ref)) if commit.status(ref)
= cache(cache_key, expires_in: 1.day) do = cache(cache_key, expires_in: 1.day) do
%li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" } %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
- if !project.repository.diffable?(blob) - if !project.repository.diffable?(blob)
.nothing-here-block This diff was suppressed by a .gitattributes entry. .nothing-here-block This diff was suppressed by a .gitattributes entry.
- elsif diff_file.collapsed? - elsif diff_file.collapsed?
- url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path)) - url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier))
.nothing-here-block.diff-collapsed{data: { diff_for_path: url } } .nothing-here-block.diff-collapsed{data: { diff_for_path: url } }
This diff is collapsed. This diff is collapsed.
%a.click-to-expand %a.click-to-expand
......
- header_title project_title(@project, "Environments", project_environments_path(@project))
- page_title "Import in progress" - page_title @project.forked? ? "Forking in progress" : "Import in progress"
.save-project-loader .save-project-loader
.center .center
%h2 %h2
......
...@@ -74,14 +74,15 @@ ...@@ -74,14 +74,15 @@
%span.badge= @merge_request.diff_size %span.badge= @merge_request.diff_size
%li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true } %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true }
%resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" } %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
.line-resolve-all{ "v-show" => "discussionCount > 0", %div
":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" } .line-resolve-all{ "v-show" => "discussionCount > 0",
%span.line-resolve-btn.is-disabled{ type: "button", ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" } %span.line-resolve-btn.is-disabled{ type: "button",
= render "shared/icons/icon_status_success.svg" ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
%span.line-resolve-text = render "shared/icons/icon_status_success.svg"
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ discussionCount | pluralize 'discussion' }} resolved %span.line-resolve-text
= render "discussions/jump_to_next" {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
= render "discussions/jump_to_next"
.tab-content#diff-notes-app .tab-content#diff-notes-app
#notes.notes.tab-pane.voting_notes #notes.notes.tab-pane.voting_notes
......
...@@ -30,11 +30,8 @@ ...@@ -30,11 +30,8 @@
.diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" } .diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
= render partial: "projects/merge_requests/conflicts/components/inline_conflict_lines" = render partial: "projects/merge_requests/conflicts/components/inline_conflict_lines"
.diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" } .diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
= render partial: "projects/merge_requests/conflicts/components/parallel_conflict_lines" %parallel-conflict-lines{ ":file" => "file" }
%div{"v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'"} %div{"v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'"}
= render partial: "projects/merge_requests/conflicts/components/diff_file_editor" = render partial: "projects/merge_requests/conflicts/components/diff_file_editor"
= render partial: "projects/merge_requests/conflicts/submit_form" = render partial: "projects/merge_requests/conflicts/submit_form"
-# Components
= render partial: 'projects/merge_requests/conflicts/components/parallel_conflict_line'
...@@ -5,11 +5,10 @@ ...@@ -5,11 +5,10 @@
%a {{line.new_line}} %a {{line.new_line}}
%td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"} %td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
%a {{line.old_line}} %a {{line.old_line}}
%td.line_content{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"} %td.line_content{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader", "v-html" => "line.richText"}
{{{line.richText}}}
%td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"} %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
%td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"} %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
%td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"} %td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
%strong {{{line.richText}}} %strong{"v-html" => "line.richText"}
%button.btn{ "@click" => "handleSelected(file, line.id, line.section)" } %button.btn{ "@click" => "handleSelected(file, line.id, line.section)" }
{{line.buttonTitle}} {{line.buttonTitle}}
%script{"id" => 'parallel-conflict-line', "type" => "text/x-template"}
%td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
%td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
%strong {{line.richText}}
%button.btn{"@click" => "handleSelected(file, line.id, line.section)"}
{{line.buttonTitle}}
%td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
{{line.lineNumber}}
%td.line_content.parallel{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
{{{line.richText}}}
%parallel-conflict-lines{"inline-template" => "true", ":file" => "file"}
%table
%tr.line_holder.parallel{"v-for" => "section in file.parallelLines"}
%td{"is"=>"parallel-conflict-line", "v-for" => "line in section", ":line" => "line", ":file" => "file"}
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
"resolved-by" => "#{note.resolved_by.try(:name)}", "resolved-by" => "#{note.resolved_by.try(:name)}",
"v-show" => "#{can_resolve || note.resolved?}", "v-show" => "#{can_resolve || note.resolved?}",
"inline-template" => true, "inline-template" => true,
"v-ref:note_#{note.id}" => true } "ref" => "note_#{note.id}" }
.note-action-button .note-action-button
= icon("spin spinner", "v-show" => "loading") = icon("spin spinner", "v-show" => "loading")
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
"@click" => "resolve", "@click" => "resolve",
":title" => "buttonText", ":title" => "buttonText",
"v-show" => "!loading", "v-show" => "!loading",
"v-el:button" => true } ":ref" => "'button'" }
= render "shared/icons/icon_status_success.svg" = render "shared/icons/icon_status_success.svg"
......
...@@ -66,8 +66,8 @@ ...@@ -66,8 +66,8 @@
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
Set Up CI Set Up CI
%li.project-repo-buttons-right %li.project-repo-buttons.right
.project-repo-buttons.project-right-buttons .project-right-buttons
- if current_user - if current_user
= render 'shared/members/access_request_buttons', source: @project = render 'shared/members/access_request_buttons', source: @project
= render "projects/buttons/koding" = render "projects/buttons/koding"
...@@ -76,7 +76,8 @@ ...@@ -76,7 +76,8 @@
= render 'projects/buttons/download', project: @project, ref: @ref = render 'projects/buttons/download', project: @project, ref: @ref
= render 'projects/buttons/dropdown' = render 'projects/buttons/dropdown'
= render 'shared/notifications/button', notification_setting: @notification_setting .pull-right
= render 'shared/notifications/button', notification_setting: @notification_setting
- if @repository.commit - if @repository.commit
.project-last-commit{ class: container_class } .project-last-commit{ class: container_class }
= render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
......
<svg width="20" height="20" class="ci-status-icon-skipped" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>Group Copy 31</title><g fill="#5C5C5C" fill-rule="evenodd"><path d="M10 17.857c4.286 0 7.857-3.571 7.857-7.857S14.286 2.143 10 2.143 2.143 5.714 2.143 10 5.714 17.857 10 17.857M10 0c5.571 0 10 4.429 10 10s-4.429 10-10 10S0 15.571 0 10 4.429 0 10 0"/><path d="M10.986 11l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.644-2.644a1.505 1.505 0 0 0 0-2.126l-2.644-2.644a1 1 0 0 0-1.414 1.414L10.986 9H6.4a1 1 0 0 0 0 2h4.586z"/></g></svg> <svg width="14" height="14" class="ci-status-icon-skipped" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>Group Copy 31</title><g fill="#5C5C5C" fill-rule="evenodd"><path d="M10 17.857c4.286 0 7.857-3.571 7.857-7.857S14.286 2.143 10 2.143 2.143 5.714 2.143 10 5.714 17.857 10 17.857M10 0c5.571 0 10 4.429 10 10s-4.429 10-10 10S0 15.571 0 10 4.429 0 10 0"/><path d="M10.986 11l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.644-2.644a1.505 1.505 0 0 0 0-2.126l-2.644-2.644a1 1 0 0 0-1.414 1.414L10.986 9H6.4a1 1 0 0 0 0 2h4.586z"/></g></svg>
- left_align = local_assigns[:left_align] - left_align = local_assigns[:left_align]
- if notification_setting - if notification_setting
.dropdown.notification-dropdown.pull-right .dropdown.notification-dropdown
= form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f| = form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
= hidden_setting_source_input(notification_setting) = hidden_setting_source_input(notification_setting)
= f.hidden_field :level, class: "notification_setting_level" = f.hidden_field :level, class: "notification_setting_level"
......
---
title: Fix project records with invalid visibility_level values
merge_request: 7391
author:
---
title: Fix no "Register" tab if ldap auth is enabled (#24038)
merge_request: 7274
author: Luc Didry
---
title: "[Fix] Extra divider issue in dropdown"
merge_request: 7398
author:
---
title: Removed gray button styling from todo buttons in sidebars
merge_request: 7387
author:
---
title: Remove additional padding on right-aligned items in MR widget.
merge_request: 7411
author: Didem Acet
---
title: Fix issue causing Labels not to appear in sidebar on MR page
merge_request: 7416
author: Alex Sanford
---
title: Fix expanding a collapsed diff when converting a symlink to a regular file
merge_request: 6953
author:
---
title: Add api endpoint `/groups/owned`
merge_request: 7103
author: Borja Aparicio
---
title: Fix cache for commit status in commits list to respect branches
merge_request: 7372
author:
---
title: Fix error when using invalid branch name when creating a new pipeline
merge_request: 7324
author:
---
title: Use 'Forking in progress' title when appropriate
merge_request: 7394
author: Philip Karpiak
---
title: Require projects before creating milestone.
merge_request: 7301
author: gfyoung
---
title: Added ability to throttle Sidekiq Jobs
merge_request: 7292
author:
...@@ -29,6 +29,8 @@ Sidekiq.configure_server do |config| ...@@ -29,6 +29,8 @@ Sidekiq.configure_server do |config|
end end
Sidekiq::Cron::Job.load_from_hash! cron_jobs Sidekiq::Cron::Job.load_from_hash! cron_jobs
Gitlab::SidekiqThrottler.execute!
# Database pool should be at least `sidekiq_concurrency` + 2 # Database pool should be at least `sidekiq_concurrency` + 2
# For more info, see: https://github.com/mperham/sidekiq/blob/master/4.0-Upgrade.md # For more info, see: https://github.com/mperham/sidekiq/blob/master/4.0-Upgrade.md
config = ActiveRecord::Base.configurations[Rails.env] || config = ActiveRecord::Base.configurations[Rails.env] ||
......
...@@ -5,10 +5,7 @@ class OnlyAllowMergeIfAllDiscussionsAreResolved < ActiveRecord::Migration ...@@ -5,10 +5,7 @@ class OnlyAllowMergeIfAllDiscussionsAreResolved < ActiveRecord::Migration
disable_ddl_transaction! disable_ddl_transaction!
def up def up
add_column_with_default(:projects, add_column :projects, :only_allow_merge_if_all_discussions_are_resolved, :boolean
:only_allow_merge_if_all_discussions_are_resolved,
:boolean,
default: false)
end end
def down def down
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddSidekiqThrottlingToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
# DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
def change
add_column :application_settings, :sidekiq_throttling_enabled, :boolean, default: false
add_column :application_settings, :sidekiq_throttling_queues, :string
add_column :application_settings, :sidekiq_throttling_factor, :decimal
end
end
class FixProjectRecordsWithInvalidVisibility < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
BATCH_SIZE = 1000
DOWNTIME = false
# This migration is idempotent and there's no sense in throwing away the
# partial result if it's interrupted
disable_ddl_transaction!
def up
projects = Arel::Table.new(:projects)
namespaces = Arel::Table.new(:namespaces)
finder =
projects.
join(namespaces, Arel::Nodes::InnerJoin).
on(projects[:namespace_id].eq(namespaces[:id])).
where(projects[:visibility_level].gt(namespaces[:visibility_level])).
project(projects[:id]).
take(BATCH_SIZE)
# MySQL requires a derived table to perform this query
nested_finder =
projects.
from(finder.as("AS projects_inner")).
project(projects[:id])
valuer =
namespaces.
where(namespaces[:id].eq(projects[:namespace_id])).
project(namespaces[:visibility_level])
# Update matching rows until none remain. The finder contains a limit.
loop do
updater = Arel::UpdateManager.new(ActiveRecord::Base).
table(projects).
set(projects[:visibility_level] => Arel::Nodes::SqlLiteral.new("(#{valuer.to_sql})")).
where(projects[:id].in(nested_finder))
num_updated = connection.exec_update(updater.to_sql, self.class.name, [])
break if num_updated == 0
end
end
def down
# no-op
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20161106185620) do ActiveRecord::Schema.define(version: 20161109150329) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -98,6 +98,9 @@ ActiveRecord::Schema.define(version: 20161106185620) do ...@@ -98,6 +98,9 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.text "help_page_text_html" t.text "help_page_text_html"
t.text "shared_runners_text_html" t.text "shared_runners_text_html"
t.text "after_sign_up_text_html" t.text "after_sign_up_text_html"
t.boolean "sidekiq_throttling_enabled", default: false
t.string "sidekiq_throttling_queues"
t.decimal "sidekiq_throttling_factor"
t.boolean "housekeeping_enabled", default: true, null: false t.boolean "housekeeping_enabled", default: true, null: false
t.boolean "housekeeping_bitmaps_enabled", default: true, null: false t.boolean "housekeeping_bitmaps_enabled", default: true, null: false
t.integer "housekeeping_incremental_repack_period", default: 10, null: false t.integer "housekeeping_incremental_repack_period", default: 10, null: false
...@@ -912,7 +915,7 @@ ActiveRecord::Schema.define(version: 20161106185620) do ...@@ -912,7 +915,7 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.boolean "has_external_wiki" t.boolean "has_external_wiki"
t.boolean "lfs_enabled" t.boolean "lfs_enabled"
t.text "description_html" t.text "description_html"
t.boolean "only_allow_merge_if_all_discussions_are_resolved", default: false, null: false t.boolean "only_allow_merge_if_all_discussions_are_resolved"
end end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
......
# GitLab operations # GitLab operations
- [Sidekiq MemoryKiller](operations/sidekiq_memory_killer.md) - [Sidekiq MemoryKiller](operations/sidekiq_memory_killer.md)
- [Sidekiq Job throttling](operations/sidekiq_job_throttling.md)
- [Cleaning up Redis sessions](operations/cleaning_up_redis_sessions.md) - [Cleaning up Redis sessions](operations/cleaning_up_redis_sessions.md)
- [Understanding Unicorn and unicorn-worker-killer](operations/unicorn.md) - [Understanding Unicorn and unicorn-worker-killer](operations/unicorn.md)
- [Moving repositories to a new location](operations/moving_repositories.md) - [Moving repositories to a new location](operations/moving_repositories.md)
# Sidekiq Job throttling
> Note: Introduced with GitLab 8.14
When your GitLab installation needs to handle tens of thousands of background
jobs, it can be convenient to throttle queues that do not need to be executed
immediately, e.g. long running jobs like Pipelines, thus allowing jobs that do
need to be executed immediately to have access to more resources.
In order to accomplish this, you can limit the amount of workers that certain
slow running queues can have available. This is what we call Sidekiq Job
Throttling. Depending on your infrastructure, you might have different slow
running queues, which is why you can choose which queues you want to throttle
and by how much you want to throttle them.
These settings are available in the Application Settings of your GitLab
installation.
![Sidekiq Job Throttling](img/sidekiq_job_throttling.png)
The throttle factor determines the maximum number of workers a queue can run on.
This value gets multiplied by `:concurrency` value set in the Sidekiq settings
and rounded up to the closest full integer.
So, for example, you set the `:concurrency` to 25 and the `Throttling factor` to
0.1, the maximum workers assigned to the selected queues would be 3.
```ruby
queue_limit = (factor * Sidekiq.options[:concurrency]).ceil
```
After enabling the job throttling, you will need to restart your GitLab
instance, in order for the changes to take effect.
\ No newline at end of file
...@@ -26,6 +26,15 @@ GET /groups ...@@ -26,6 +26,15 @@ 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
```
## 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.
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
contributing to documentation. contributing to documentation.
- [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations - [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations
- [Testing standards and style guidelines](testing.md) - [Testing standards and style guidelines](testing.md)
- [UI guide](ui_guide.md) for building GitLab with existing CSS styles and elements - [UX guide](ux_guide/README.md) for building GitLab with existing CSS styles and elements
- [Frontend guidelines](frontend.md) - [Frontend guidelines](frontend.md)
- [SQL guidelines](sql.md) for working with SQL queries - [SQL guidelines](sql.md) for working with SQL queries
- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers - [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
......
# Basics
## Contents
* [Responsive](#responsive)
* [Typography](#typography)
* [Icons](#icons)
* [Color](#color)
* [Motion](#motion)
* [Voice and tone](#voice-and-tone)
---
## Responsive
GitLab is a responsive experience that works well across all screen sizes, from mobile devices to large monitors. In order to provide a great user experience, the core functionality (browsing files, creating issues, writing comments, etc.) must be available at all resolutions. However, due to size limitations, some secondary functionality may be hidden on smaller screens. Please keep this functionality limited to rare actions that aren't expected to be needed on small devices.
---
## Typography
### Primary typeface
GitLab's main typeface used throughout the UI is **Source Sans Pro**. We support both the bold and regular weight.
![Source Sans Pro sample](img/sourcesanspro-sample.png)
### Monospace typeface
This is the typeface used for code blocks. GitLab uses the OS default font.
- **Menlo** (Mac)
- **Consolas** (Windows)
- **Liberation Mono** (Linux)
![Monospace font sample](img/monospacefont-sample.png)
---
## Icons
GitLab uses Font Awesome icons throughout our interface.
![Trash icon](img/icon-trash.png)
The trash icon is used for destructive actions that deletes information.
![Edit icon](img/icon-edit.png)
The pencil icon is used for editing content such as comments.
![Notification icon](img/icon-notification.png)
The bell icon is for notifications, such as Todos.
![Subscribe icon](img/icon-subscribe.png)
The eye icon is for subscribing to updates. For example, you can subscribe to a label and get updated on issues with that label.
![RSS icon](img/icon-rss.png)
The standard RSS icon is used for linking to RSS/atom feeds.
![Close icon](img/icon-close.png)
An 'x' is used for closing UI elements such as dropdowns.
![Add icon](img/icon-add.png)
A plus is used when creating new objects, such as issues, projects, etc.
> TODO: update this section, add more general guidance to icon usage and personality, etc.
---
## Color
![Blue](img/color-blue.png)
Blue is used to highlight primary active elements (such as current tab), as well as other organization and managing commands.
![Green](img/color-green.png)
Green is for actions that create new objects.
![Orange](img/color-orange.png)
Orange is used for warnings
![Red](img/color-red.png)
Red is reserved for delete and other destructive commands
![Grey](img/color-grey.png)
Grey, and white (depending on context) is used for netral, secondary elements
> TODO: Establish a perspective for color in terms of our personality and rationalize with Marketing usage.
---
## Motion
Motion is a tool to help convey important relationships, changes or transitions between elements. It should be used sparingly and intentionally, highlighting the right elements at the right moment.
> TODO: Determine a more concrete perspective on motion, create consistent easing/timing curves to follow.
---
## Voice and tone
The copy for GitLab is clear and direct. We strike a clear balance between professional and friendly. We can empathesize with users (such as celebrating completing all Todos), and remain respectful of the importance of the work. We are that trusted, friendly coworker that is helpful and understanding.
# Components
## Contents
* [Tooltips](#tooltips)
* [Anchor links](#anchor-links)
* [Buttons](#buttons)
* [Dropdowns](#dropdowns)
* [Counts](#counts)
* [Lists](#lists)
* [Tables](#tables)
* [Blocks](#blocks)
* [Panels](#panels)
* [Alerts](#alerts)
* [Forms](#forms)
* [File holders](#file-holders)
* [Data formats](#data-formats)
---
## Tooltips
### Usage
A tooltip should only be added if additional information is required.
![Tooltip usage](img/tooltip-usage.png)
### Placement
By default, tooltips should be placed below the element that they refer to. However, if there is not enough space in the viewpoint, the tooltip should be moved to the side as needed.
![Tooltip placement location](img/tooltip-placement.png)
---
## Anchor links
Anchor links are used for navigational actions and lone, secondary commands (such as 'Reset filters' on the Issues List) when deemed appropriate by the UX team.
### States
#### Rest
Primary links are blue in their rest state. Secondary links (such as the time stamp on comments) are a neutral gray color in rest. Details on the main GitLab navigation links can be found on the [features](features.md#navigation) page.
#### Hover
An underline should always be added on hover. A gray link becomes blue on hover.
#### Focus
The focus state should match the hover state.
![Anchor link states ](img/components-anchorlinks.png)
---
## Buttons
Buttons communicate the command that will occur when the user clicks on them.
### Types
#### Primary
Primary buttons communicate the main call to action. There should only be one call to action in any given experience. Visually, primary buttons are conveyed with a full background fill
![Primary button example](img/button-primary.png)
#### Secondary
Secondary buttons are for alternative commands. They should be conveyed by a button with an stroke, and no background fill.
![Secondary button example](img/button-secondary.png)
### Icon and text treatment
Text should be in sentence case, where only the first word is capitalized. "Create issue" is correct, not "Create Issue". Buttons should only contain an icon or a text, not both.
>>>
TODO: Rationalize this. Ensure that we still believe this.
>>>
### Colors
Follow the color guidance on the [basics](basics.md#color) page. The default color treatment is the white/grey button.
---
## Dropdowns
Dropdowns are used to allow users to choose one (or many) options from a list of options. If this list of options is more 20, there should generally be a way to search through and filter the options (see the complex filter dropdowns below.)
>>>
TODO: Will update this section when the new filters UI is implemented.
>>>
![Dropdown states](img/components-dropdown.png)
---
## Counts
A count element is used in navigation contexts where it is helpful to indicate the count, or number of items, in a list. Always use the [`number_with_delimiter`][number_with_delimiter] helper to display counts in the UI.
![Counts example](img/components-counts.png)
[number_with_delimiter]: http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_with_delimiter
---
## Lists
Lists are used where ever there is a single column of information to display. Ths [issues list](https://gitlab.com/gitlab-org/gitlab-ce/issues) is an example of a important list in the GitLab UI.
### Types
Simple list using .content-list
![Simple list](img/components-simplelist.png)
List with avatar, title and description using .content-list
![List with avatar](img/components-listwithavatar.png)
List with hover effect .well-list
![List with hover effect](img/components-listwithhover.png)
List inside panel
![List inside panel](img/components-listinsidepanel.png)
---
## Tables
When the information is too complex for a list, with multiple columns of information, a table can be used. For example, the [pipelines page](https://gitlab.com/gitlab-org/gitlab-ce/pipelines) uses a table.
![Table](img/components-table.png)
---
## Blocks
Blocks are a way to group related information.
### Types
#### Content blocks
Content blocks (`.content-block`) are the basic grouping of content. They are commonly used in [lists](#lists), and are separated by a botton border.
![Content block](img/components-contentblock.png)
#### Row content blocks
A background color can be added to this blocks. For example, items in the [issue list](https://gitlab.com/gitlab-org/gitlab-ce/issues) have a green background if they were created recently. Below is an example of a gray content block with side padding using `.row-content-block`.
![Row content block](img/components-rowcontentblock.png)
#### Cover blocks
Cover blocks are generally used to create a heading element for a page, such as a new project, or a user profile page. Below is a cover block (`.cover-block`) for the profile page with an avatar, name and description.
![Cover block](img/components-coverblock.png)
---
## Panels
>>>
TODO: Catalog how we are currently using panels and rationalize how they relate to alerts
>>>
![Panels](img/components-panels.png)
---
## Alerts
>>>
TODO: Catalog how we are currently using alerts
>>>
![Alerts](img/components-alerts.png)
---
## Forms
There are two options shown below regarding the positioning of labels in forms. Both are options to consider based on context and available size. However, it is important to have a consistent treatment of labels in the same form.
### Types
#### Labels stack vertically
Form (`form`) with label rendered above input.
![Vertical form](img/components-verticalform.png)
#### Labels side-by-side
Horizontal form (`form.horizontal-form`) with label rendered inline with input.
![Horizontal form](img/components-horizontalform.png)
---
## File holders
A file holder (`.file-holder`) is used to show the contents of a file inline on a page of GitLab.
![File Holder component](img/components-fileholder.png)
---
## Data formats
### Dates
#### Exact
Format for exacts dates should be ‘Mon DD, YYYY’, such as the examples below.
![Exact date](img/components-dateexact.png)
#### Relative
This format relates how long since an action has occurred. The exact date can be shown as a tooltip on hover.
![Relative date](img/components-daterelative.png)
### References
Referencing GitLab items depends on a symbol for each type of item. Typing that symbol will invoke a dropdown that allows you to search for and autocomplete the item you were looking for. References are shown as [links](#links) in context, and hovering on them shows the full title or name of the item.
![Hovering on a reference](img/components-referencehover.png)
#### `%` Milestones
![Milestone reference](img/components-referencemilestone.png)
#### `#` Issues
![Issue reference](img/components-referenceissues.png)
#### `!` Merge Requests
![Merge request reference](img/components-referencemrs.png)
#### `~` Labels
![Labels reference](img/components-referencelabels.png)
#### `@` People
![People reference](img/components-referencepeople.png)
> TODO: Open issue: Some commit references use monospace fonts, but others don't. Need to standardize this.
# Features
## Contents
* [Navigation](#navigation)
* [Filtering](#filtering)
* [Search results](#search-results)
* [Conversations](#conversations)
* [Empty states](#empty-states)
---
## Navigation
### Global navigation
The global navigation is accessible via the menu button on the top left of the screen, and can be pinned to keep it open. It contains a consistent list of pages that allow you to view content that is across GitLab. For example, you can view your todos, issues and merge requests across projects and groups.
![Global nav](img/features-globalnav.png)
### Contextual navigation
The navigation in the header is contextual to each page. These options change depending on if you are looking at a project, group, or settings page. There should be no more than 10 items on a level in the contextual navigation, allowing it to comfortably fit on a typical laptop screen. There can be up to too levels of navigation. Each sub nav group should be a self-contained group of functionality. For example, everything related to the issue tracker should be under the 'Issue' tab, while everything relating to the wiki will be grouped under the 'Wiki' tab. The names used for each section should be short and easy to remember, ideally 1-2 words in length.
![Contextual nav](img/features-contextualnav.png)
### Information architecture
The [GitLab Product Map](https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png) shows a visual representation of the information architecture for GitLab.
---
## Filtering
Today, lists are filtered by a series of dropdowns. Some of these dropdowns allow multiselect (labels), while others allow you to filter to one option (milestones). However, we are currently implementing a [new model](https://gitlab.com/gitlab-org/gitlab-ce/issues/21747) for this, and will update the guide when it is ready.
![Filters](img/features-filters.png)
---
## Search results
### Global search
[Global search](https://gitlab.com/search?group_id=&project_id=13083&repository_ref=&scope=issues&search=mobile) allows you to search across items in a project, or even across multiple projects. You can switch tabs to filter on type of object, or filter by group.
### List search
There are several core lists in the GitLab experience, such as the Issue list and the Merge Request list. You are also able to [filter and search these lists](https://gitlab.com/gitlab-org/gitlab-ce/issues?utf8=%E2%9C%93&search=mobile). This UI will be updated with the [new filtering model](https://gitlab.com/gitlab-org/gitlab-ce/issues/21747).
---
## Empty states
Empty states need to be considered in the design of features. They are vital to helping onboard new users, making the experience feel more approachable and understandable. Empty states should feel inviting and provide just enough information to get people started. There should be a single call to action and a clear explanation of what to use the feature for.
![Empty states](img/features-emptystates.png)
# GitLab UX Guide
The goal of this guide is to provide standards, principles and in-depth information to design beautiful and effective GitLab features. This will be a living document, and we welcome contributions, feedback and suggestions.
## Design
---
### [Principles](principles.md)
These guiding principles set a solid foundation for our design system, and should remain relatively stable over multiple releases. They should be referenced as new design patterns are created.
---
### [Basics](basics.md)
The basic ingredients of our experience establish our personality and feel. This section includes details about typography, color, and motion.
---
### [Components](components.md)
Components are the controls that make up the GitLab experience, including guidance around buttons, links, dropdowns, etc.
---
### [Surfaces](surfaces.md)
The GitLab experience is broken apart into several surfaces. Each of these surfaces is designated for a specific scope or type of content. Examples include the header, global menu, side pane, etc.
---
### [Features](features.md)
The previous building blocks are combined into complete features in the GitLab UX. Examples include our navigation, filters, search results, and empty states.
---
## Research
---
### [Users](users.md)
How we think about the variety of users of GitLab, from small to large teams, comparing opensource usage to enterprise, etc.
---
## Other
---
### [Tips for designers](tips.md)
Tips for exporting assets, and other guidance.
---
### [Resources](resources.md)
Resources for GitLab UX
# Principles
These are the guiding principles that we should strive for to establish a solid foundation for the GitLab experience.
## Professional and productive
GitLab is a tool to support what people do, day in, day out. We need to respect the importance of their work, and avoid gimicky details.
## Minimal and efficient
While work can get complicated, GitLab is about bringing a sharp focus, helping our customers know what matters now.
## Immediately recognizable
When you look at any screen, you should know immediately that it is GitLab. Our personality is strong and consistent across product and marketing experiences.
## Human and quirky
We need to build empathy with our users, understanding their state of mind, and connect with them at a human level. Quirkiness is part of our DNA, and we should embrace it in the right moments and contexts.
> TODO: Ensure these principles align well with the goals of the Marketing team
# Resources
## GitLab UI development kit
We created a page inside GitLab where you can check commonly used html and css elements.
When you run GitLab instance locally - just visit http://localhost:3000/help/ui page to see UI examples
you can use during GitLab development.
## Design repository
All design files are stored in the [gitlab-design](https://gitlab.com/gitlab-org/gitlab-design)
repository and maintained by GitLab UX designers.
\ No newline at end of file
# Surfaces
## Contents
* [Header](#header)
* [Global menu](#global-menu)
* [Side pane](#side-pane)
* [Content area](#content-area)
---
![Surfaces UX](img/surfaces-ux.png)
## Global menu
This menu is to navigate to pages that contain content global to GitLab.
---
## Header
The header contains 3 main elements: Project switching and searching, user account avatar and settings, and a contextual menu that changes based on the current page.
![Surfaces Header](img/surfaces-header.png)
---
## Side pane
The side pane holds supporting information and meta data for the information in the content area.
---
## Content area
The main content of the page. The content area can include other surfaces.
### Item title bar
The item title bar contains the top level information to identify the item, such as the name, id and status.
![Item title](img/surfaces-contentitemtitle.png)
### Item system information
The system information block contains relevant system controlled information.
![Item system information](img/surfaces-systeminformationblock.png)
# Tips
## Contents
* [SVGs](#svgs)
---
## SVGs
When exporting SVGs, be sure to follow the following guidelines:
1. Convert all strokes to outlines.
2. Use pathfinder tools to combine overlapping paths and create compound paths.
3. SVGs that are limited to one color should be exported without a fill color so the color can be set using CSS.
4. Ensure that exported SVGs have been run through an [SVG cleaner](https://github.com/RazrFalcon/SVGCleaner) to remove unused elements and attributes.
You can open your svg in a text editor to ensure that it is clean.
Incorrect files will look like this:
```xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="17px" viewBox="0 0 16 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
<title>Group</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" fill="#7E7C7C">
<path d="M15.1111,1 L0.8891,1 C0.3981,1 0.0001,1.446 0.0001,1.996 L0.0001,15.945 C0.0001,16.495 0.3981,16.941 0.8891,16.941 L15.1111,16.941 C15.6021,16.941 16.0001,16.495 16.0001,15.945 L16.0001,1.996 C16.0001,1.446 15.6021,1 15.1111,1 L15.1111,1 L15.1111,1 Z M14.0001,6.0002 L14.0001,14.949 L2.0001,14.949 L2.0001,6.0002 L14.0001,6.0002 Z M14.0001,4.0002 L14.0001,2.993 L2.0001,2.993 L2.0001,4.0002 L14.0001,4.0002 Z" id="Combined-Shape"></path>
<polygon id="Fill-11" points="3 2.0002 5 2.0002 5 0.0002 3 0.0002"></polygon>
<polygon id="Fill-16" points="11 2.0002 13 2.0002 13 0.0002 11 0.0002"></polygon>
<path d="M5.37709616,11.5511984 L6.92309616,12.7821984 C7.35112915,13.123019 7.97359761,13.0565604 8.32002627,12.6330535 L10.7740263,9.63305349 C11.1237073,9.20557058 11.0606364,8.57555475 10.6331535,8.22587373 C10.2056706,7.87619272 9.57565475,7.93926361 9.22597373,8.36674651 L6.77197373,11.3667465 L8.16890384,11.2176016 L6.62290384,9.98660159 C6.19085236,9.6425813 5.56172188,9.71394467 5.21770159,10.1459962 C4.8736813,10.5780476 4.94504467,11.2071781 5.37709616,11.5511984 L5.37709616,11.5511984 Z" id="Stroke-21"></path>
</g>
</g>
</svg>
```
Correct file will look like this:
```xml
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 17" enable-background="new 0 0 16 17"><path d="m15.1 1h-2.1v-1h-2v1h-6v-1h-2v1h-2.1c-.5 0-.9.5-.9 1v14c0 .6.4 1 .9 1h14.2c.5 0 .9-.4.9-1v-14c0-.5-.4-1-.9-1m-1.1 14h-12v-9h12v9m0-11h-12v-1h12v1"/><path d="m5.4 11.6l1.5 1.2c.4.3 1.1.3 1.4-.1l2.5-3c.3-.4.3-1.1-.1-1.4-.5-.4-1.1-.3-1.5.1l-1.8 2.2-.8-.6c-.4-.3-1.1-.3-1.4.2-.3.4-.3 1 .2 1.4"/></svg>
```
> TODO: Checkout [https://github.com/svg/svgo](https://github.com/svg/svgo)
# Users
> TODO: Create personas. Understand the similarities and differences across the below spectrums.
## Users by organization
- Enterprise
- Medium company
- Small company
- Open source communities
## Users by role
- Admin
- Manager
- Developer
...@@ -10,7 +10,7 @@ To enable the Shibboleth OmniAuth provider you must: ...@@ -10,7 +10,7 @@ To enable the Shibboleth OmniAuth provider you must:
1. Configure Apache shibboleth module. Installation and configuration of module it self is out of scope of this document. 1. Configure Apache shibboleth module. Installation and configuration of module it self is out of scope of this document.
Check https://wiki.shibboleth.net/ for more info. Check https://wiki.shibboleth.net/ for more info.
1. You can find Apache config in gitlab-recipes (https://github.com/gitlabhq/gitlab-recipes/blob/master/web-server/apache/gitlab-ssl.conf) 1. You can find Apache config in gitlab-recipes (https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache)
Following changes are needed to enable shibboleth: Following changes are needed to enable shibboleth:
......
...@@ -212,5 +212,8 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project ...@@ -212,5 +212,8 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project
1. [Support Path](support/README.md) 1. [Support Path](support/README.md)
1. [Sales Path (redirect to sales handbook)](https://about.gitlab.com/handbook/sales-onboarding/) 1. [Sales Path (redirect to sales handbook)](https://about.gitlab.com/handbook/sales-onboarding/)
1. [User Training](training/user_training.md)
1. [GitLab Flow Training](training/gitlab_flow.md)
1. [Training Topics](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/university/training/topics/)
1. [GitLab architecture for noobs](https://dev.gitlab.org/gitlab/gitlabhq/blob/master/doc/development/architecture.md) 1. [GitLab architecture for noobs](https://dev.gitlab.org/gitlab/gitlabhq/blob/master/doc/development/architecture.md)
1. [Client Assessment of GitLab versus GitHub](https://docs.google.com/a/gitlab.com/spreadsheets/d/18cRF9Y5I6I7Z_ab6qhBEW55YpEMyU4PitZYjomVHM-M/edit?usp=sharing) 1. [Client Assessment of GitLab versus GitHub](https://docs.google.com/a/gitlab.com/spreadsheets/d/18cRF9Y5I6I7Z_ab6qhBEW55YpEMyU4PitZYjomVHM-M/edit?usp=sharing)
# GitLab Flow
- A simplified branching strategy
- All features and fixes first go to master
- Allows for 'production' or 'stable' branches
- Bug fixes/hot fix patches are cherry-picked from master
---
# Feature branches
- Create a feature/bugfix branch to do all work
- Use merge requests to merge to master
![inline](gitlab_flow/feature_branches.png)
---
# Production branch
- One, long-running production release branch
as opposed to individual stable branches
- Consider creating a tag for each version that gets deployed
---
# Production branch
![inline](gitlab_flow/production_branch.png)
---
# Release branch
- Useful if you release software to customers
- When preparing a new release, create stable branch
from master
- Consider creating a tag for each version
- Cherry-pick critical bug fixes to stable branch for patch release
- Never commit bug fixes directly to stable branch
---
# Release branch
![inline](gitlab_flow/release_branches.png)
---
# More details
Blog post on 'GitLab Flow' at
[http://doc.gitlab.com/ee/workflow/gitlab_flow.html](http://doc.gitlab.com/ee/workflow/gitlab_flow.html)
# GitLab Training Material
All GitLab training material is stored in markdown format. Slides are
generated using [Deskset](http://www.decksetapp.com/).
All training material is open to public contribution.
## Additional Resources
1. GitLab Documentation [http://docs.gitlab.com](http://docs.gitlab.com/)
2. GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/guis)
3. Pro git book [http://git-scm.com/book](http://git-scm.com/book)
4. Platzi Course [https://courses.platzi.com/courses/git-gitlab/](https://courses.platzi.com/courses/git-gitlab/)
5. Code School tutorial [http://try.github.io/](http://try.github.io/)
6. Contact Us - [subscribers@gitlab.com](subscribers@gitlab.com)
# Agile and Git
----------
## Agile
Lean software development methods focused on collaboration and interaction
with fast and smaller deployment cycles.
----------
## Where Git comes in
Git is an excellent tool for an Agile team considering that it allows
decentralized and simultaneous development.
----------
### Branching And Workflows
Branching in an Agile environment usually happens around user stories with one
or more developers working on it.
If more than one developer then another branch for each developer is also used
with his/her initials, and US id.
After its tested merge into master and remove the branch.
----------
## What about GitLab
Tools like GitLab enhance collaboration by adding dialog around code mainly
through issues and merge requests.
# Bisect
----------
## Bisect
- Find a commit that introduced a bug
- Works through a process of elimination
- Specify a known good and bad revision to begin
----------
## Bisect
1. Start the bisect process
2. Enter the bad revision (usually latest commit)
3. Enter a known good revision (commit/branch)
4. Run code to see if bug still exists
5. Tell bisect the result
6. Repeat the previous 2 items until you find the offending commit
----------
## Setup
```
mkdir bisect-ex
cd bisect-ex
touch index.html
git add -A
git commit -m "starting out"
vi index.html
# Add all good
git add -A
git commit -m "second commit"
vi index.html
# Add all good 2
git add -A
git commit -m "third commit"
vi index.html
```
----------
```
# Add all good 3
git add -A
git commit -m "fourth commit"
vi index.html
# This looks bad
git add -A
git commit -m "fifth commit"
vi index.html
# Really bad
git add -A
git commit -m "sixth commit"
vi index.html
# again just bad
git add -A
git commit -m "seventh commit"
```
----------
## Commands
```
git bisect start
# Test your code
git bisect bad
git bisect next
# Say yes to the warning
# Test
git bisect good
# Test
git bisect bad
# Test
git bisect good
# done
git bisect reset
```
# Cherry Pick
----------
## Cherry Pick
- Given an existing commit on one branch, apply the change to another branch
- Useful for backporting bug fixes to previous release branches
- Make the commit on the master branch and pick in to stable
----------
## Cherry Pick
1. Check out a new 'stable' branch from 'master'
1. Change back to 'master'
1. Edit '`cherry_pick.rb`' and commit the changes.
1. Check commit log to get the commit SHA
1. Check out the 'stable' branch
1. Cherry pick the commit using the SHA obtained earlier
----------
## Commands
```bash
git checkout master
git checkout -b stable
git checkout master
# Edit `cherry_pick.rb`
git add cherry_pick.rb
git commit -m 'Fix bugs in cherry_pick.rb'
git log
# Copy commit SHA
git checkout stable
git cherry-pick <commit SHA>
```
# Configure your environment
----------
## Install
- **Windows**
- Install 'Git for Windows' from https://git-for-windows.github.io
- **Mac**
- Type '`git`' in the Terminal application.
- If it's not installed, it will prompt you to install it.
- **Linux**
```bash
sudo yum install git-all
```
```bash
sudo apt-get install git-all
```
----------
## Configure Git
One-time configuration of the Git client
```bash
git config --global user.name "Your Name"
git config --global user.email you@example.com
```
----------
## Configure SSH Key
```bash
ssh-keygen -t rsa -b 4096 -C "you@computer-name"
```
```bash
# You will be prompted for the following information. Press enter to accept the defaults. Defaults appear in parentheses.
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/you/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /Users/you/.ssh/id_rsa.
Your public key has been saved in /Users/you/.ssh/id_rsa.pub.
The key fingerprint is:
39:fc:ce:94:f4:09:13:95:64:9a:65:c1:de:05:4d:01 you@computer-name
```
Copy your public key and add it to your GitLab profile
```bash
cat ~/.ssh/id_rsa.pub
```
```bash
ssh-rsa AAAAB3NzaC1yc2EAAAADAQEL17Ufacg8cDhlQMS5NhV8z3GHZdhCrZbl4gz you@example.com
```
# Explore GitLab projects
----------
- Dashboard
- User Preferences
- Issues
- Milestones and Labels
- Manage project members
- Project settings
# Feature branching
----------
- Efficient parallel workflow for teams
- Develop each feature in a branch
- Keeps changes isolated
- Consider a 1-to-1 link to issues
- Push branches to the server frequently
- Hint: This is a cheap backup for your work-in-progress code
----------
## Feature branching
1. Create a new feature branch called 'squash_some_bugs'
1. Edit '`bugs.rb`' and remove all the bugs.
1. Commit
1. Push
----------
## Commands
```
git checkout -b squash_some_bugs
# Edit `bugs.rb`
git status
git add bugs.rb
git commit -m 'Fix some buggy code'
git push origin squash_some_bugs
```
# Getting Started
----------
## Instantiating Repositories
* Create a new repository by instantiating it through
```bash
git init
```
* Copy an existing project by cloning the repository through
```bash
git clone <url>
```
----------
## Central Repos
* To instantiate a central repository a `--bare` flag is required.
* Bare repositories don't allow file editing or committing changes.
* Create a bare repo with
```bash
git init --bare project-name.git
```
----------
## Instantiate workflow with clone
1. Create a project in your user namespace
- Choose to import from 'Any Repo by URL' and use
https://gitlab.com/gitlab-org/training-examples.git
2. Create a '`Workspace`' directory in your home directory.
3. Clone the '`training-examples`' project
----------
## Commands
```
mkdir ~/workspace
cd ~/workspace
git clone git@gitlab.example.com:<username>/training-examples.git
cd training-examples
```
----------
## Git concepts
**Untracked files**
New files that Git has not been told to track previously.
**Working area**
Files that have been modified but are not committed.
**Staging area**
Modified files that have been marked to go in the next commit.
----------
## Committing Workflow
1. Edit '`edit_this_file.rb`' in '`training-examples`'
1. See it listed as a changed file (working area)
1. View the differences
1. Stage the file
1. Commit
1. Push the commit to the remote
1. View the git log
----------
## Commands
```
# Edit `edit_this_file.rb`
git status
git diff
git add <file>
git commit -m 'My change'
git push origin master
git log
```
----------
## Note
* git fetch vs pull
* Pull is git fetch + git merge
# Git Add
----------
## Git Add
Adds content to the index or staging area.
* Adds a list of file
```bash
git add <files>
```
* Adds all files including deleted ones
```bash
git add -A
```
----------
## Git add continued
* Add all text files in current dir
```bash
git add *.txt
```
* Add all text file in the project
```bash
git add "*.txt*"
```
* Adds all files in directory
```bash
git add views/layouts/
```
# Git introduction
----------
## Intro
https://git-scm.com/about
- Distributed version control
- Does not rely on connection to a central server
- Many copies of the complete history
- Powerful branching and merging
- Adapts to nearly any workflow
- Fast, reliable and stable file format
----------
## Help!
Use the tools at your disposal when you get stuck.
- Use '`git help <command>`' command
- Use Google
- Read documentation at https://git-scm.com
# Git Log
----------
Git log lists commit history. It allows searching and filtering.
* Initiate log
```
git log
```
* Retrieve set number of records:
```
git log -n 2
```
* Search commits by author. Allows user name or a regular expression.
```
git log --author="user_name"
```
----------
* Search by comment message.
```
git log --grep="<pattern>"
```
* Search by date
```
git log --since=1.month.ago --until=3.weeks.ago
```
----------
## Git Log Workflow
1. Change to workspace directory
2. Clone the multi runner projects
3. Change to project dir
4. Search by author
5. Search by date
6. Combine
----------
## Commands
```
cd ~/workspace
git clone git@gitlab.com:gitlab-org/gitlab-ci-multi-runner.git
cd gitlab-ci-multi-runner
git log --author="Travis"
git log --since=1.month.ago --until=3.weeks.ago
git log --since=1.month.ago --until=1.day.ago --author="Travis"
```
# GitLab Flow
----------
- A simplified branching strategy
- All features and fixes first go to master
- Allows for 'production' or 'stable' branches
- Bug fixes/hot fix patches are cherry-picked from master
----------
### Feature branches
- Create a feature/bugfix branch to do all work
- Use merge requests to merge to master
![inline](http://gitlab.com/gitlab-org/University/raw/5baea0fe222a915d0500e40747d35eb18681cdc3/training/gitlab_flow/feature_branches.png)
----------
## Production branch
- One, long-running production release branch
as opposed to individual stable branches
- Consider creating a tag for each version that gets deployed
----------
## Production branch
![inline](http://gitlab.com/gitlab-org/University/raw/5baea0fe222a915d0500e40747d35eb18681cdc3/training/gitlab_flow/production_branch.png)
----------
## Release branch
- Useful if you release software to customers
- When preparing a new release, create stable branch
from master
- Consider creating a tag for each version
- Cherry-pick critical bug fixes to stable branch for patch release
- Never commit bug fixes directly to stable branch
----------
![inline](http://gitlab.com/gitlab-org/University/raw/5baea0fe222a915d0500e40747d35eb18681cdc3/training/gitlab_flow/release_branches.png)
----------
## More details
Blog post on 'GitLab Flow' at
[http://doc.gitlab.com/ee/workflow/gitlab_flow.html](http://doc.gitlab.com/ee/workflow/gitlab_flow.html)
# Merge conflicts
----------
- Happen often
- Learning to fix conflicts is hard
- Practice makes perfect
- Force push after fixing conflicts. Be careful!
----------
## Merge conflicts
1. Checkout a new branch and edit `conflicts.rb`. Add 'Line4' and 'Line5'.
2. Commit and push
3. Checkout master and edit `conflicts.rb`. Add 'Line6' and 'Line7' below 'Line3'.
4. Commit and push to master
5. Create a merge request and watch it fail
6. Rebase our new branch with master
7. Fix conflicts on the `conflicts.rb` file.
8. Stage the file and continue rebasing
9. Force push the changes
10. Finally continue with the Merge Request
----------
## Commands
```
git checkout -b conflicts_branch
# vi conflicts.rb
# Add 'Line4' and 'Line5'
git commit -am "add line4 and line5"
git push origin conflicts_branch
git checkout master
# vi conflicts.rb
# Add 'Line6' and 'Line7'
git commit -am "add line6 and line7"
git push origin master
```
Create a merge request on the GitLab web UI. You'll see a conflict warning.
```
git checkout conflicts_branch
git fetch
git rebase master
# Fix conflicts by editing the files.
git add conflicts.rb
# No need to commit this file
git rebase --continue
# Remember that we have rewritten our commit history so we
# need to force push so that our remote branch is restructured
git push origin conflicts_branch -f
```
----------
## Note
* When to use 'git merge' and when to use 'git rebase'
* Rebase when updating your branch with master
* Merge when bringing changes from feature to master
* Reference: https://www.atlassian.com/git/tutorials/merging-vs-rebasing/
# Merge requests
----------
- When you want feedback create a merge request
- Target is the default branch (usually master)
- Assign or mention the person you would like to review
- Add 'WIP' to the title if it's a work in progress
- When accepting, always delete the branch
- Anyone can comment, not just the assignee
- Push corrections to the same branch
----------
## Merge requests
**Create your first merge request**
1. Use the blue button in the activity feed
1. View the diff (changes) and leave a comment
1. Push a new commit to the same branch
1. Review the changes again and notice the update
----------
## Feedback and Collaboration
- Merge requests are a time for feedback and collaboration
- Giving feedback is hard
- Be as kind as possible
- Receiving feedback is hard
- Be as receptive as possible
- Feedback is about the best code, not the person. You are not your code
----------
## Feedback and Collaboration
Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests:
[https://github.com/thoughtbot/guides/tree/master/code-review](https://github.com/thoughtbot/guides/tree/master/code-review)
See GitLab merge requests for examples:
[https://gitlab.com/gitlab-org/gitlab-ce/merge_requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests)
# Rollback Commits
----------
## Undo Commits
* Undo last commit putting everything back into the staging area.
```
git reset --soft HEAD^
```
* Add files and change message with:
```
git commit --amend -m "New Message"
```
----------
* Undo last and remove changes
```
git reset --hard HEAD^
```
* Same as last one but for two commits back
```
git reset --hard HEAD^^
```
** Don't reset after pushing **
----------
## Reset Workflow
1. Edit file again 'edit_this_file.rb'
2. Check status
3. Add and commit with wrong message
4. Check log
5. Amend commit
6. Check log
7. Soft reset
8. Check log
9. Pull for updates
10. Push changes
----------
## Commands
```
# Change file edit_this_file.rb
git status
git commit -am "kjkfjkg"
git log
git commit --amend -m "New comment added"
git log
git reset --soft HEAD^
git log
git pull origin master
git push origin master
```
----------
## Note
* git revert vs git reset
* Reset removes the commit while revert removes the changes but leaves the commit
* Revert is safer considering we can revert a revert
```
# Changed file
git commit -am "bug introduced"
git revert HEAD
# New commit created reverting changes
# Now we want to re apply the reverted commit
git log # take hash from the revert commit
git revert <rev commit hash>
# reverted commit is back (new commit created again)
```
# Git Stash
----------
We use git stash to store our changes when they are not ready to be committed
and we need to change to a different branch.
* Stash
```
git stash save
# or
git stash
# or with a message
git stash save "this is a message to display on the list"
```
* Apply stash to keep working on it
```
git stash apply
# or apply a specific one from out stack
git stash apply stash@{3}
```
----------
* Every time we save a stash it gets stacked so by using list we can see all our
stashes.
```
git stash list
# or for more information (log methods)
git stash list --stat
```
* To clean our stack we need to manually remove them.
```
# drop top stash
git stash drop
# or
git stash drop <name>
# to clear all history we can use
git stash clear
```
----------
* Apply and drop on one command
```
git stash pop
```
* If we meet conflicts we need to either reset or commit our changes.
* Conflicts through `pop` will not drop a stash afterwards.
----------
## Git Stash
1. Modify a file
2. Stage file
3. Stash it
4. View our stash list
5. Confirm no pending changes through status
5. Apply with pop
6. View list to confirm changes
----------
## Commands
```
# Modify edit_this_file.rb file
git add .
git stash save "Saving changes from edit this file"
git stash list
git status
git stash pop
git stash list
git status
```
## Subtree
----------
## Subtree
* Used when there are nested repositories.
* Not recommended when the amount of dependencies is too large
* For these cases we need a dependency control system
* Command are painfully long so aliases are necessary
----------
## Subtree Aliases
* Add: git subtree add --prefix <target-folder> <url> <branch> --squash
* Pull: git subtree add --prefix <target-folder> <url> <branch> --squash
* Push: git subtree add --prefix <target-folder> <url> <branch>
* Ex: git config alias.sbp 'subtree pull --prefix st /
git@gitlab.com:balameb/subtree-nested-example.git master --squash'
----------
```
# Add an alias
# Add
git config alias.sba 'subtree add --prefix st /
git@gitlab.com:balameb/subtree-nested-example.git master --squash'
# Pull
git config alias.sbpl 'subtree pull --prefix st /
git@gitlab.com:balameb/subtree-nested-example.git master --squash'
# Push
git config alias.sbph 'subtree push --prefix st /
git@gitlab.com:balameb/subtree-nested-example.git master'
# Adding this subtree adds a st dir with a readme
git sba
vi st/README.md
# Edit file
git status shows differences
```
----------
```
# Adding, or committing won't change the sub repo at remote
# even if we push
git add -A
git commit -m "Adding to subtree readme"
# Push to subtree repo
git sbph
# now we can check our remote sub repo
```
# Tags
----------
- Useful for marking deployments and releases
- Annotated tags are an unchangeable part of Git history
- Soft/lightweight tags can be set and removed at will
- Many projects combine an anotated release tag with a stable branch
- Consider setting deployment/release tags automatically
----------
# Tags
- Create a lightweight tag
- Create an annotated tag
- Push the tags to the remote repository
**Additional resources**
[http://git-scm.com/book/en/Git-Basics-Tagging](http://git-scm.com/book/en/Git-Basics-Tagging)
----------
# Commands
```
git checkout master
# Lightweight tag
git tag my_lightweight_tag
# Annotated tag
git tag -a v1.0 -m ‘Version 1.0’
git tag
git push origin --tags
```
# Unstage
----------
## Unstage
* To remove files from stage use reset HEAD. Where HEAD is the last commit of the current branch.
```bash
git reset HEAD <file>
```
* This will unstage the file but maintain the modifications. To revert the file back to the state it was in before the changes we can use:
```bash
git checkout -- <file>
```
----------
* To remove a file from disk and repo use 'git rm' and to rm a dir use the '-r' flag.
```
git rm '*.txt'
git rm -r <dirname>
```
* If we want to remove a file from the repository but keep it on disk, say we forgot to add it to our `.gitignore` file then use `--cache`.
```
git rm <filename> --cache
```
# GitLab Git Workshop
---
# Agenda
1. Brief history of Git
1. GitLab walkthrough
1. Configure your environment
1. Workshop
---
# Git introduction
https://git-scm.com/about
- Distributed version control
- Does not rely on connection to a central server
- Many copies of the complete history
- Powerful branching and merging
- Adapts to nearly any workflow
- Fast, reliable and stable file format
---
# Help!
Use the tools at your disposal when you get stuck.
- Use '`git help <command>`' command
- Use Google
- Read documentation at https://git-scm.com
---
# GitLab Walkthrough
![fit](logo.png)
---
# Configure your environment
- Windows: Install 'Git for Windows'
> https://git-for-windows.github.io
- Mac: Type '`git`' in the Terminal application.
> If it's not installed, it will prompt you to install it.
- Debian: '`sudo apt-get install git-all`'
or Red Hat '`sudo yum install git-all`'
---
# Git Workshop
## Overview
1. Configure Git
1. Configure SSH Key
1. Create a project
1. Committing
1. Feature branching
1. Merge requests
1. Feedback and Collaboration
---
# Configure Git
One-time configuration of the Git client
```bash
git config --global user.name "Your Name"
git config --global user.email you@example.com
```
---
# Configure SSH Key
```bash
ssh-keygen -t rsa -b 4096 -C "you@computer-name"
```
```bash
# You will be prompted for the following information. Press enter to accept the defaults. Defaults appear in parentheses.
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/you/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /Users/you/.ssh/id_rsa.
Your public key has been saved in /Users/you/.ssh/id_rsa.pub.
The key fingerprint is:
39:fc:ce:94:f4:09:13:95:64:9a:65:c1:de:05:4d:01 you@computer-name
```
Copy your public key and add it to your GitLab profile
```bash
cat ~/.ssh/id_rsa.pub
```
```bash
ssh-rsa AAAAB3NzaC1yc2EAAAADAQEL17Ufacg8cDhlQMS5NhV8z3GHZdhCrZbl4gz you@example.com
```
---
# Create a project
- Create a project in your user namespace
- Choose to import from 'Any Repo by URL' and use
https://gitlab.com/gitlab-org/training-examples.git
- Create a '`development`' or '`workspace`' directory in your home directory.
- Clone the '`training-examples`' project
---
# Commands
```
mkdir ~/development
cd ~/development
-or-
mkdir ~/workspace
cd ~/workspace
git clone git@gitlab.example.com:<username>/training-examples.git
cd training-examples
```
---
# Git concepts
**Untracked files**
New files that Git has not been told to track previously.
**Working area**
Files that have been modified but are not committed.
**Staging area**
Modified files that have been marked to go in the next commit.
---
# Committing
1. Edit '`edit_this_file.rb`' in '`training-examples`'
1. See it listed as a changed file (working area)
1. View the differences
1. Stage the file
1. Commit
1. Push the commit to the remote
1. View the git log
---
# Commands
```
# Edit `edit_this_file.rb`
git status
git diff
git add <file>
git commit -m 'My change'
git push origin master
git log
```
---
# Feature branching
- Efficient parallel workflow for teams
- Develop each feature in a branch
- Keeps changes isolated
- Consider a 1-to-1 link to issues
- Push branches to the server frequently
- Hint: This is a cheap backup for your work-in-progress code
---
# Feature branching
1. Create a new feature branch called 'squash_some_bugs'
1. Edit '`bugs.rb`' and remove all the bugs.
1. Commit
1. Push
---
# Commands
```
git checkout -b squash_some_bugs
# Edit `bugs.rb`
git status
git add bugs.rb
git commit -m 'Fix some buggy code'
git push origin squash_some_bugs
```
---
# Merge requests
- When you want feedback create a merge request
- Target is the ‘default’ branch (usually master)
- Assign or mention the person you would like to review
- Add 'WIP' to the title if it's a work in progress
- When accepting, always delete the branch
- Anyone can comment, not just the assignee
- Push corrections to the same branch
---
# Merge requests
**Create your first merge request**
1. Use the blue button in the activity feed
1. View the diff (changes) and leave a comment
1. Push a new commit to the same branch
1. Review the changes again and notice the update
---
# Feedback and Collaboration
- Merge requests are a time for feedback and collaboration
- Giving feedback is hard
- Be as kind as possible
- Receiving feedback is hard
- Be as receptive as possible
- Feedback is about the best code, not the person. You are not your code
---
# Feedback and Collaboration
Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests:
[https://github.com/thoughtbot/guides/tree/master/code-review](https://github.com/thoughtbot/guides/tree/master/code-review)
See GitLab merge requests for examples:
[https://gitlab.com/gitlab-org/gitlab-ce/merge_requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests)
---
# Explore GitLab projects
![fit](logo.png)
- Dashboard
- User Preferences
- ReadMe, Changelog, License shortcuts
- Issues
- Milestones and Labels
- Manage project members
- Project settings
---
# Tags
- Useful for marking deployments and releases
- Annotated tags are an unchangeable part of Git history
- Soft/lightweight tags can be set and removed at will
- Many projects combine an anotated release tag with a stable branch
- Consider setting deployment/release tags automatically
---
# Tags
- Create a lightweight tag
- Create an annotated tag
- Push the tags to the remote repository
**Additional resources**
[http://git-scm.com/book/en/Git-Basics-Tagging](http://git-scm.com/book/en/Git-Basics-Tagging)
---
# Commands
```
git checkout master
# Lightweight tag
git tag my_lightweight_tag
# Annotated tag
git tag -a v1.0 -m ‘Version 1.0’
git tag
git push origin --tags
```
---
# Merge conflicts
- Happen often
- Learning to fix conflicts is hard
- Practice makes perfect
- Force push after fixing conflicts. Be careful!
---
# Merge conflicts
1. Checkout a new branch and edit `conflicts.rb`. Add 'Line4' and 'Line5'.
1. Commit and push
1. Checkout master and edit `conflicts.rb`. Add 'Line6' and 'Line7' below 'Line3'.
1. Commit and push to master
1. Create a merge request
---
# Merge conflicts
After creating a merge request you should notice that conflicts exist. Resolve
the conflicts locally by rebasing.
```
git rebase master
# Fix conflicts by editing the files.
git add conflicts.rb
git commit -m 'Fix conflicts'
git rebase --continue
git push origin <branch> -f
```
---
# Rebase with squash
You may end up with a commit log that looks like this:
```
Fix issue #13
Test
Fix
Fix again
Test
Test again
Does this work?
```
Squash these in to meaningful commits using an interactive rebase.
---
# Rebase with squash
Squash the commits on the same branch we used for the merge conflicts step.
```
git rebase -i master
```
In the editor, leave the first commit as 'pick' and set others to 'fixup'.
---
# Questions?
![fit](logo.png)
Thank you for your hard work!
**Additional Resources**
GitLab Documentation [http://docs.gitlab.com](http://docs.gitlab.com/)
GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/guis)
Pro git book [http://git-scm.com/book](http://git-scm.com/book)
Platzi Course [https://courses.platzi.com/courses/git-gitlab/](https://courses.platzi.com/courses/git-gitlab/)
Code School tutorial [http://try.github.io/](http://try.github.io/)
Contact Us - [subscribers@gitlab.com](subscribers@gitlab.com)
...@@ -26,6 +26,16 @@ module API ...@@ -26,6 +26,16 @@ module API
present @groups, with: Entities::Group present @groups, with: Entities::Group
end end
# Get list of owned groups for authenticated user
#
# Example Request:
# GET /groups/owned
get '/owned' do
@groups = current_user.owned_groups
@groups = paginate @groups
present @groups, with: Entities::Group, user: current_user
end
# Create group. Available only for users who can create groups. # Create group. Available only for users who can create groups.
# #
# Parameters: # Parameters:
......
...@@ -11,19 +11,25 @@ module API ...@@ -11,19 +11,25 @@ module API
else milestones else milestones
end end
end end
params :optional_params do
optional :description, type: String, desc: 'The description of the milestone'
optional :due_date, type: String, desc: 'The due date of the milestone'
end
end end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects do resource :projects do
# Get a list of project milestones desc 'Get a list of project milestones' do
# success Entities::Milestone
# Parameters: end
# id (required) - The ID of a project params do
# state (optional) - Return "active" or "closed" milestones optional :state, type: String, values: %w[active closed all], default: 'all',
# Example Request: desc: 'Return "active", "closed", or "all" milestones'
# GET /projects/:id/milestones optional :iid, type: Integer, desc: 'The IID of the milestone'
# GET /projects/:id/milestones?iid=42 end
# GET /projects/:id/milestones?state=active
# GET /projects/:id/milestones?state=closed
get ":id/milestones" do get ":id/milestones" do
authorize! :read_milestone, user_project authorize! :read_milestone, user_project
...@@ -34,34 +40,31 @@ module API ...@@ -34,34 +40,31 @@ module API
present paginate(milestones), with: Entities::Milestone present paginate(milestones), with: Entities::Milestone
end end
# Get a single project milestone desc 'Get a single project milestone' do
# success Entities::Milestone
# Parameters: end
# id (required) - The ID of a project params do
# milestone_id (required) - The ID of a project milestone requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
# Example Request: end
# GET /projects/:id/milestones/:milestone_id
get ":id/milestones/:milestone_id" do get ":id/milestones/:milestone_id" do
authorize! :read_milestone, user_project authorize! :read_milestone, user_project
@milestone = user_project.milestones.find(params[:milestone_id]) milestone = user_project.milestones.find(params[:milestone_id])
present @milestone, with: Entities::Milestone present milestone, with: Entities::Milestone
end end
# Create a new project milestone desc 'Create a new project milestone' do
# success Entities::Milestone
# Parameters: end
# id (required) - The ID of the project params do
# title (required) - The title of the milestone requires :title, type: String, desc: 'The title of the milestone'
# description (optional) - The description of the milestone use :optional_params
# due_date (optional) - The due date of the milestone end
# Example Request:
# POST /projects/:id/milestones
post ":id/milestones" do post ":id/milestones" do
authorize! :admin_milestone, user_project authorize! :admin_milestone, user_project
required_attributes! [:title] milestone_params = declared(params, include_parent_namespaces: false)
attrs = attributes_for_keys [:title, :description, :due_date]
milestone = ::Milestones::CreateService.new(user_project, current_user, attrs).execute milestone = ::Milestones::CreateService.new(user_project, current_user, milestone_params).execute
if milestone.valid? if milestone.valid?
present milestone, with: Entities::Milestone present milestone, with: Entities::Milestone
...@@ -70,22 +73,23 @@ module API ...@@ -70,22 +73,23 @@ module API
end end
end end
# Update an existing project milestone desc 'Update an existing project milestone' do
# success Entities::Milestone
# Parameters: end
# id (required) - The ID of a project params do
# milestone_id (required) - The ID of a project milestone requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
# title (optional) - The title of a milestone optional :title, type: String, desc: 'The title of the milestone'
# description (optional) - The description of a milestone optional :state_event, type: String, values: %w[close activate],
# due_date (optional) - The due date of a milestone desc: 'The state event of the milestone '
# state_event (optional) - The state event of the milestone (close|activate) use :optional_params
# Example Request: at_least_one_of :title, :description, :due_date, :state_event
# PUT /projects/:id/milestones/:milestone_id end
put ":id/milestones/:milestone_id" do put ":id/milestones/:milestone_id" do
authorize! :admin_milestone, user_project authorize! :admin_milestone, user_project
attrs = attributes_for_keys [:title, :description, :due_date, :state_event] milestone_params = declared(params, include_parent_namespaces: false, include_missing: false)
milestone = user_project.milestones.find(params[:milestone_id])
milestone = ::Milestones::UpdateService.new(user_project, current_user, attrs).execute(milestone) milestone = user_project.milestones.find(milestone_params.delete(:milestone_id))
milestone = ::Milestones::UpdateService.new(user_project, current_user, milestone_params).execute(milestone)
if milestone.valid? if milestone.valid?
present milestone, with: Entities::Milestone present milestone, with: Entities::Milestone
...@@ -94,21 +98,20 @@ module API ...@@ -94,21 +98,20 @@ module API
end end
end end
# Get all issues for a single project milestone desc 'Get all issues for a single project milestone' do
# success Entities::Issue
# Parameters: end
# id (required) - The ID of a project params do
# milestone_id (required) - The ID of a project milestone requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
# Example Request: end
# GET /projects/:id/milestones/:milestone_id/issues
get ":id/milestones/:milestone_id/issues" do get ":id/milestones/:milestone_id/issues" do
authorize! :read_milestone, user_project authorize! :read_milestone, user_project
@milestone = user_project.milestones.find(params[:milestone_id]) milestone = user_project.milestones.find(params[:milestone_id])
finder_params = { finder_params = {
project_id: user_project.id, project_id: user_project.id,
milestone_title: @milestone.title milestone_title: milestone.title
} }
issues = IssuesFinder.new(current_user, finder_params).execute issues = IssuesFinder.new(current_user, finder_params).execute
......
module API module API
# Runners API
class Runners < Grape::API class Runners < Grape::API
before { authenticate! } before { authenticate! }
resource :runners do resource :runners do
# Get runners available for user desc 'Get runners available for user' do
# success Entities::Runner
# Example Request: end
# GET /runners params do
optional :scope, type: String, values: %w[active paused online],
desc: 'The scope of specific runners to show'
end
get do get do
runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: ['specific', 'shared']) runners = filter_runners(current_user.ci_authorized_runners, params[:scope], without: ['specific', 'shared'])
present paginate(runners), with: Entities::Runner present paginate(runners), with: Entities::Runner
end end
# Get all runners - shared and specific desc 'Get all runners - shared and specific' do
# success Entities::Runner
# Example Request: end
# GET /runners/all params do
optional :scope, type: String, values: %w[active paused online specific shared],
desc: 'The scope of specific runners to show'
end
get 'all' do get 'all' do
authenticated_as_admin! authenticated_as_admin!
runners = filter_runners(Ci::Runner.all, params[:scope]) runners = filter_runners(Ci::Runner.all, params[:scope])
present paginate(runners), with: Entities::Runner present paginate(runners), with: Entities::Runner
end end
# Get runner's details desc "Get runner's details" do
# success Entities::RunnerDetails
# Parameters: end
# id (required) - The ID of ther runner params do
# Example Request: requires :id, type: Integer, desc: 'The ID of the runner'
# GET /runners/:id end
get ':id' do get ':id' do
runner = get_runner(params[:id]) runner = get_runner(params[:id])
authenticate_show_runner!(runner) authenticate_show_runner!(runner)
...@@ -36,33 +41,37 @@ module API ...@@ -36,33 +41,37 @@ module API
present runner, with: Entities::RunnerDetails, current_user: current_user present runner, with: Entities::RunnerDetails, current_user: current_user
end end
# Update runner's details desc "Update runner's details" do
# success Entities::RunnerDetails
# Parameters: end
# id (required) - The ID of ther runner params do
# description (optional) - Runner's description requires :id, type: Integer, desc: 'The ID of the runner'
# active (optional) - Runner's status optional :description, type: String, desc: 'The description of the runner'
# tag_list (optional) - Array of tags for runner optional :active, type: Boolean, desc: 'The state of a runner'
# Example Request: optional :tag_list, type: Array[String], desc: 'The list of tags for a runner'
# PUT /runners/:id optional :run_untagged, type: Boolean, desc: 'Flag indicating the runner can execute untagged jobs'
optional :locked, type: Boolean, desc: 'Flag indicating the runner is locked'
at_least_one_of :description, :active, :tag_list, :run_untagged, :locked
end
put ':id' do put ':id' do
runner = get_runner(params[:id]) runner = get_runner(params.delete(:id))
authenticate_update_runner!(runner) authenticate_update_runner!(runner)
attrs = attributes_for_keys [:description, :active, :tag_list, :run_untagged, :locked] runner_params = declared(params, include_missing: false)
if runner.update(attrs)
if runner.update(runner_params)
present runner, with: Entities::RunnerDetails, current_user: current_user present runner, with: Entities::RunnerDetails, current_user: current_user
else else
render_validation_error!(runner) render_validation_error!(runner)
end end
end end
# Remove runner desc 'Remove a runner' do
# success Entities::Runner
# Parameters: end
# id (required) - The ID of ther runner params do
# Example Request: requires :id, type: Integer, desc: 'The ID of the runner'
# DELETE /runners/:id end
delete ':id' do delete ':id' do
runner = get_runner(params[:id]) runner = get_runner(params[:id])
authenticate_delete_runner!(runner) authenticate_delete_runner!(runner)
...@@ -72,28 +81,31 @@ module API ...@@ -72,28 +81,31 @@ module API
end end
end end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects do resource :projects do
before { authorize_admin_project } before { authorize_admin_project }
# Get runners available for project desc 'Get runners available for project' do
# success Entities::Runner
# Example Request: end
# GET /projects/:id/runners params do
optional :scope, type: String, values: %w[active paused online specific shared],
desc: 'The scope of specific runners to show'
end
get ':id/runners' do get ':id/runners' do
runners = filter_runners(Ci::Runner.owned_or_shared(user_project.id), params[:scope]) runners = filter_runners(Ci::Runner.owned_or_shared(user_project.id), params[:scope])
present paginate(runners), with: Entities::Runner present paginate(runners), with: Entities::Runner
end end
# Enable runner for project desc 'Enable a runner for a project' do
# success Entities::Runner
# Parameters: end
# id (required) - The ID of the project params do
# runner_id (required) - The ID of the runner requires :runner_id, type: Integer, desc: 'The ID of the runner'
# Example Request: end
# POST /projects/:id/runners/:runner_id
post ':id/runners' do post ':id/runners' do
required_attributes! [:runner_id]
runner = get_runner(params[:runner_id]) runner = get_runner(params[:runner_id])
authenticate_enable_runner!(runner) authenticate_enable_runner!(runner)
...@@ -106,13 +118,12 @@ module API ...@@ -106,13 +118,12 @@ module API
end end
end end
# Disable project's runner desc "Disable project's runner" do
# success Entities::Runner
# Parameters: end
# id (required) - The ID of the project params do
# runner_id (required) - The ID of the runner requires :runner_id, type: Integer, desc: 'The ID of the runner'
# Example Request: end
# DELETE /projects/:id/runners/:runner_id
delete ':id/runners/:runner_id' do delete ':id/runners/:runner_id' do
runner_project = user_project.runner_projects.find_by(runner_id: params[:runner_id]) runner_project = user_project.runner_projects.find_by(runner_id: params[:runner_id])
not_found!('Runner') unless runner_project not_found!('Runner') unless runner_project
......
module API module API
# Users API
class Session < Grape::API class Session < Grape::API
# Login to get token desc 'Login to get token' do
# success Entities::UserLogin
# Parameters: end
# login (*required) - user login params do
# email (*required) - user email optional :login, type: String, desc: 'The username'
# password (required) - user password optional :email, type: String, desc: 'The email of the user'
# requires :password, type: String, desc: 'The password of the user'
# Example Request: at_least_one_of :login, :email
# POST /session end
post "/session" do post "/session" do
user = Gitlab::Auth.find_with_user_password(params[:email] || params[:login], params[:password]) user = Gitlab::Auth.find_with_user_password(params[:email] || params[:login], params[:password])
......
module API module API
# Triggers API
class Triggers < Grape::API class Triggers < Grape::API
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects do resource :projects do
# Trigger a GitLab project build desc 'Trigger a GitLab project build' do
# success Entities::TriggerRequest
# Parameters: end
# id (required) - The ID of a CI project params do
# ref (required) - The name of project's branch or tag requires :ref, type: String, desc: 'The commit sha or name of a branch or tag'
# token (required) - The uniq token of trigger requires :token, type: String, desc: 'The unique token of trigger'
# variables (optional) - The list of variables to be injected into build optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
# Example Request: end
# POST /projects/:id/trigger/builds
post ":id/trigger/builds" do post ":id/trigger/builds" do
required_attributes! [:ref, :token]
project = Project.find_with_namespace(params[:id]) || Project.find_by(id: params[:id]) project = Project.find_with_namespace(params[:id]) || Project.find_by(id: params[:id])
trigger = Ci::Trigger.find_by_token(params[:token].to_s) trigger = Ci::Trigger.find_by_token(params[:token].to_s)
not_found! unless project && trigger not_found! unless project && trigger
...@@ -22,10 +21,6 @@ module API ...@@ -22,10 +21,6 @@ module API
# validate variables # validate variables
variables = params[:variables] variables = params[:variables]
if variables if variables
unless variables.is_a?(Hash)
render_api_error!('variables needs to be a hash', 400)
end
unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) } unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
render_api_error!('variables needs to be a map of key-valued strings', 400) render_api_error!('variables needs to be a map of key-valued strings', 400)
end end
...@@ -44,31 +39,24 @@ module API ...@@ -44,31 +39,24 @@ module API
end end
end end
# Get triggers list desc 'Get triggers list' do
# success Entities::Trigger
# Parameters: end
# id (required) - The ID of a project
# page (optional) - The page number for pagination
# per_page (optional) - The value of items per page to show
# Example Request:
# GET /projects/:id/triggers
get ':id/triggers' do get ':id/triggers' do
authenticate! authenticate!
authorize! :admin_build, user_project authorize! :admin_build, user_project
triggers = user_project.triggers.includes(:trigger_requests) triggers = user_project.triggers.includes(:trigger_requests)
triggers = paginate(triggers)
present triggers, with: Entities::Trigger present paginate(triggers), with: Entities::Trigger
end end
# Get specific trigger of a project desc 'Get specific trigger of a project' do
# success Entities::Trigger
# Parameters: end
# id (required) - The ID of a project params do
# token (required) - The `token` of a trigger requires :token, type: String, desc: 'The unique token of trigger'
# Example Request: end
# GET /projects/:id/triggers/:token
get ':id/triggers/:token' do get ':id/triggers/:token' do
authenticate! authenticate!
authorize! :admin_build, user_project authorize! :admin_build, user_project
...@@ -79,12 +67,9 @@ module API ...@@ -79,12 +67,9 @@ module API
present trigger, with: Entities::Trigger present trigger, with: Entities::Trigger
end end
# Create trigger desc 'Create a trigger' do
# success Entities::Trigger
# Parameters: end
# id (required) - The ID of a project
# Example Request:
# POST /projects/:id/triggers
post ':id/triggers' do post ':id/triggers' do
authenticate! authenticate!
authorize! :admin_build, user_project authorize! :admin_build, user_project
...@@ -94,13 +79,12 @@ module API ...@@ -94,13 +79,12 @@ module API
present trigger, with: Entities::Trigger present trigger, with: Entities::Trigger
end end
# Delete trigger desc 'Delete a trigger' do
# success Entities::Trigger
# Parameters: end
# id (required) - The ID of a project params do
# token (required) - The `token` of a trigger requires :token, type: String, desc: 'The unique token of trigger'
# Example Request: end
# DELETE /projects/:id/triggers/:token
delete ':id/triggers/:token' do delete ':id/triggers/:token' do
authenticate! authenticate!
authorize! :admin_build, user_project authorize! :admin_build, user_project
......
...@@ -23,6 +23,10 @@ module Gitlab ...@@ -23,6 +23,10 @@ module Gitlab
settings || fake_application_settings settings || fake_application_settings
end end
def sidekiq_throttling_enabled?
current_application_settings.sidekiq_throttling_enabled
end
def fake_application_settings def fake_application_settings
OpenStruct.new( OpenStruct.new(
default_projects_limit: Settings.gitlab['default_projects_limit'], default_projects_limit: Settings.gitlab['default_projects_limit'],
...@@ -50,6 +54,7 @@ module Gitlab ...@@ -50,6 +54,7 @@ module Gitlab
repository_checks_enabled: true, repository_checks_enabled: true,
container_registry_token_expire_delay: 5, container_registry_token_expire_delay: 5,
user_default_external: false, user_default_external: false,
sidekiq_throttling_enabled: false,
) )
end end
......
...@@ -126,7 +126,7 @@ module Gitlab ...@@ -126,7 +126,7 @@ module Gitlab
repository.blob_at(commit.id, file_path) repository.blob_at(commit.id, file_path)
end end
def cache_key def file_identifier
"#{file_path}-#{new_file}-#{deleted_file}-#{renamed_file}" "#{file_path}-#{new_file}-#{deleted_file}-#{renamed_file}"
end end
end end
......
...@@ -39,7 +39,7 @@ module Gitlab ...@@ -39,7 +39,7 @@ module Gitlab
# hashes that represent serialized diff lines. # hashes that represent serialized diff lines.
# #
def cache_highlight!(diff_file) def cache_highlight!(diff_file)
item_key = diff_file.cache_key item_key = diff_file.file_identifier
if highlight_cache[item_key] if highlight_cache[item_key]
highlight_diff_file_from_cache!(diff_file, highlight_cache[item_key]) highlight_diff_file_from_cache!(diff_file, highlight_cache[item_key])
......
...@@ -2,39 +2,38 @@ ...@@ -2,39 +2,38 @@
module Gitlab module Gitlab
# Checks if a set of migrations requires downtime or not. # Checks if a set of migrations requires downtime or not.
class EeCompatCheck class EeCompatCheck
CE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ce.git'.freeze
EE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze EE_REPO = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
CHECK_DIR = Rails.root.join('ee_compat_check')
MAX_FETCH_DEPTH = 500
IGNORED_FILES_REGEX = /(VERSION|CHANGELOG\.md:\d+)/.freeze
attr_reader :ce_branch, :check_dir, :ce_repo attr_reader :repo_dir, :patches_dir, :ce_repo, :ce_branch
def initialize(branch:, check_dir:, ce_repo: nil) def initialize(branch:, ce_repo: CE_REPO)
@repo_dir = CHECK_DIR.join('repo')
@patches_dir = CHECK_DIR.join('patches')
@ce_branch = branch @ce_branch = branch
@check_dir = check_dir @ce_repo = ce_repo
@ce_repo = ce_repo || 'https://gitlab.com/gitlab-org/gitlab-ce.git'
end end
def check def check
ensure_ee_repo ensure_ee_repo
delete_patches ensure_patches_dir
generate_patch(ce_branch, ce_patch_full_path) generate_patch(ce_branch, ce_patch_full_path)
Dir.chdir(check_dir) do Dir.chdir(repo_dir) do
step("In the #{check_dir} directory") step("In the #{repo_dir} directory")
step("Pulling latest master", %w[git pull --ff-only origin master])
status = catch(:halt_check) do status = catch(:halt_check) do
ce_branch_compat_check! ce_branch_compat_check!
delete_ee_branch_locally!
delete_ee_branch_locally
ee_branch_presence_check! ee_branch_presence_check!
ee_branch_compat_check! ee_branch_compat_check!
end end
delete_ee_branch_locally delete_ee_branch_locally!
delete_patches
if status.nil? if status.nil?
true true
...@@ -47,20 +46,43 @@ module Gitlab ...@@ -47,20 +46,43 @@ module Gitlab
private private
def ensure_ee_repo def ensure_ee_repo
if Dir.exist?(check_dir) if Dir.exist?(repo_dir)
step("#{check_dir} already exists") step("#{repo_dir} already exists")
else else
cmd = %W[git clone --branch master --single-branch --depth 1 #{EE_REPO} #{check_dir}] cmd = %W[git clone --branch master --single-branch --depth 200 #{EE_REPO} #{repo_dir}]
step("Cloning #{EE_REPO} into #{check_dir}", cmd) step("Cloning #{EE_REPO} into #{repo_dir}", cmd)
end end
end end
def ce_branch_compat_check! def ensure_patches_dir
cmd = %W[git apply --check #{ce_patch_full_path}] FileUtils.mkdir_p(patches_dir)
status = step("Checking if #{ce_patch_name} applies cleanly to EE/master", cmd) end
def generate_patch(branch, patch_path)
FileUtils.rm(patch_path, force: true)
depth = 0
loop do
depth += 50
cmd = %W[git fetch --depth #{depth} origin --prune +refs/heads/master:refs/remotes/origin/master]
Gitlab::Popen.popen(cmd)
_, status = Gitlab::Popen.popen(%w[git merge-base FETCH_HEAD HEAD])
raise "#{branch} is too far behind master, please rebase it!" if depth >= MAX_FETCH_DEPTH
break if status.zero?
end
if status.zero? step("Generating the patch against master in #{patch_path}")
puts ce_applies_cleanly_msg(ce_branch) output, status = Gitlab::Popen.popen(%w[git format-patch FETCH_HEAD --stdout])
throw(:halt_check, :ko) unless status.zero?
File.write(patch_path, output)
throw(:halt_check, :ko) unless File.exist?(patch_path)
end
def ce_branch_compat_check!
if check_patch(ce_patch_full_path).zero?
puts applies_cleanly_msg(ce_branch)
throw(:halt_check) throw(:halt_check)
end end
end end
...@@ -80,10 +102,8 @@ module Gitlab ...@@ -80,10 +102,8 @@ module Gitlab
step("Checking out origin/#{ee_branch}", %W[git checkout -b #{ee_branch} FETCH_HEAD]) step("Checking out origin/#{ee_branch}", %W[git checkout -b #{ee_branch} FETCH_HEAD])
generate_patch(ee_branch, ee_patch_full_path) generate_patch(ee_branch, ee_patch_full_path)
cmd = %W[git apply --check #{ee_patch_full_path}]
status = step("Checking if #{ee_patch_name} applies cleanly to EE/master", cmd)
unless status.zero? unless check_patch(ee_patch_full_path).zero?
puts puts
puts ee_branch_doesnt_apply_cleanly_msg puts ee_branch_doesnt_apply_cleanly_msg
...@@ -91,50 +111,49 @@ module Gitlab ...@@ -91,50 +111,49 @@ module Gitlab
end end
puts puts
puts ee_applies_cleanly_msg puts applies_cleanly_msg(ee_branch)
end end
def generate_patch(branch, filepath) def check_patch(patch_path)
FileUtils.rm(filepath, force: true) step("Checking out master", %w[git checkout master])
step("Reseting to latest master", %w[git reset --hard origin/master])
depth = 0 step("Checking if #{patch_path} applies cleanly to EE/master")
loop do output, status = Gitlab::Popen.popen(%W[git apply --check #{patch_path}])
depth += 10
step("Fetching origin/master", %W[git fetch origin master --depth=#{depth}])
status = step("Finding merge base with master", %W[git merge-base FETCH_HEAD #{branch}])
break if status.zero? || depth > 500
end
raise "#{branch} is too far behind master, please rebase it!" if depth > 500 unless status.zero?
failed_files = output.lines.reduce([]) do |memo, line|
if line.start_with?('error: patch failed:')
file = line.sub(/\Aerror: patch failed: /, '')
memo << file unless file =~ IGNORED_FILES_REGEX
end
memo
end
step("Generating the patch against master") if failed_files.empty?
output, status = Gitlab::Popen.popen(%w[git format-patch FETCH_HEAD --stdout]) status = 0
throw(:halt_check, :ko) unless status.zero? else
puts "\nConflicting files:"
failed_files.each do |file|
puts " - #{file}"
end
end
end
File.write(filepath, output) status
throw(:halt_check, :ko) unless File.exist?(filepath)
end end
def delete_ee_branch_locally def delete_ee_branch_locally!
command(%w[git checkout master]) command(%w[git checkout master])
step("Deleting the local #{ee_branch} branch", %W[git branch -D #{ee_branch}]) step("Deleting the local #{ee_branch} branch", %W[git branch -D #{ee_branch}])
end end
def delete_patches
step("Deleting #{ce_patch_full_path}")
FileUtils.rm(ce_patch_full_path, force: true)
step("Deleting #{ee_patch_full_path}")
FileUtils.rm(ee_patch_full_path, force: true)
end
def ce_patch_name def ce_patch_name
@ce_patch_name ||= "#{ce_branch}.patch" @ce_patch_name ||= "#{ce_branch}.patch"
end end
def ce_patch_full_path def ce_patch_full_path
@ce_patch_full_path ||= File.expand_path(ce_patch_name, check_dir) @ce_patch_full_path ||= patches_dir.join(ce_patch_name)
end end
def ee_branch def ee_branch
...@@ -146,15 +165,18 @@ module Gitlab ...@@ -146,15 +165,18 @@ module Gitlab
end end
def ee_patch_full_path def ee_patch_full_path
@ee_patch_full_path ||= File.expand_path(ee_patch_name, check_dir) @ee_patch_full_path ||= patches_dir.join(ee_patch_name)
end end
def step(desc, cmd = nil) def step(desc, cmd = nil)
puts "\n=> #{desc}\n" puts "\n=> #{desc}\n"
if cmd if cmd
start = Time.now
puts "\n$ #{cmd.join(' ')}" puts "\n$ #{cmd.join(' ')}"
command(cmd) status = command(cmd)
puts "\nFinished in #{Time.now - start} seconds"
status
end end
end end
...@@ -165,12 +187,12 @@ module Gitlab ...@@ -165,12 +187,12 @@ module Gitlab
status status
end end
def ce_applies_cleanly_msg(ce_branch) def applies_cleanly_msg(branch)
<<-MSG.strip_heredoc <<-MSG.strip_heredoc
================================================================= =================================================================
🎉 Congratulations!! 🎉 🎉 Congratulations!! 🎉
The #{ce_branch} branch applies cleanly to EE/master! The #{branch} branch applies cleanly to EE/master!
Much ❤️!! Much ❤️!!
=================================================================\n =================================================================\n
...@@ -211,7 +233,7 @@ module Gitlab ...@@ -211,7 +233,7 @@ module Gitlab
# In the EE repo # In the EE repo
$ git fetch origin $ git fetch origin
$ git checkout -b #{ee_branch} FETCH_HEAD $ git checkout -b #{ee_branch} origin/master
$ git fetch #{ce_repo} #{ce_branch} $ git fetch #{ce_repo} #{ce_branch}
$ git cherry-pick SHA # Repeat for all the commits you want to pick $ git cherry-pick SHA # Repeat for all the commits you want to pick
...@@ -245,17 +267,5 @@ module Gitlab ...@@ -245,17 +267,5 @@ module Gitlab
=================================================================\n =================================================================\n
MSG MSG
end end
def ee_applies_cleanly_msg
<<-MSG.strip_heredoc
=================================================================
🎉 Congratulations!! 🎉
The #{ee_branch} branch applies cleanly to EE/master!
Much ❤️!!
=================================================================\n
MSG
end
end end
end end
module Gitlab
class SidekiqThrottler
class << self
def execute!
if Gitlab::CurrentSettings.sidekiq_throttling_enabled?
Gitlab::CurrentSettings.current_application_settings.sidekiq_throttling_queues.each do |queue|
Sidekiq::Queue[queue].limit = queue_limit
end
end
end
private
def queue_limit
@queue_limit ||=
begin
factor = Gitlab::CurrentSettings.current_application_settings.sidekiq_throttling_factor
(factor * Sidekiq.options[:concurrency]).ceil
end
end
end
end
end
namespace :gitlab do namespace :gitlab do
namespace :dev do namespace :dev do
desc 'Checks if the branch would apply cleanly to EE' desc 'Checks if the branch would apply cleanly to EE'
task ee_compat_check: :environment do task :ee_compat_check, [:branch] => :environment do |_, args|
return if defined?(Gitlab::License) opts =
return unless ENV['CI'] if ENV['CI']
{
branch: ENV['CI_BUILD_REF_NAME'],
ce_repo: ENV['CI_BUILD_REPO']
}
else
unless args[:branch]
puts "Must specify a branch as an argument".color(:red)
exit 1
end
args
end
success = if Gitlab::EeCompatCheck.new(opts || {}).check
Gitlab::EeCompatCheck.new(
branch: ENV['CI_BUILD_REF_NAME'],
check_dir: File.expand_path('ee-compat-check', __dir__),
ce_repo: ENV['CI_BUILD_REPO']
).check
if success
exit 0 exit 0
else else
exit 1 exit 1
......
...@@ -39,6 +39,17 @@ describe Projects::MergeRequestsController do ...@@ -39,6 +39,17 @@ describe Projects::MergeRequestsController do
end end
end end
shared_examples "loads labels" do |action|
it "loads labels into the @labels variable" do
get action,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
id: merge_request.iid,
format: 'html'
expect(assigns(:labels)).not_to be_nil
end
end
describe "GET show" do describe "GET show" do
shared_examples "export merge as" do |format| shared_examples "export merge as" do |format|
it "does generally work" do it "does generally work" do
...@@ -51,6 +62,8 @@ describe Projects::MergeRequestsController do ...@@ -51,6 +62,8 @@ describe Projects::MergeRequestsController do
expect(response).to be_success expect(response).to be_success
end end
it_behaves_like "loads labels", :show
it "generates it" do it "generates it" do
expect_any_instance_of(MergeRequest).to receive(:"to_#{format}") expect_any_instance_of(MergeRequest).to receive(:"to_#{format}")
...@@ -406,6 +419,8 @@ describe Projects::MergeRequestsController do ...@@ -406,6 +419,8 @@ describe Projects::MergeRequestsController do
get :diffs, params.merge(extra_params) get :diffs, params.merge(extra_params)
end end
it_behaves_like "loads labels", :diffs
context 'with default params' do context 'with default params' do
context 'as html' do context 'as html' do
before { go(format: 'html') } before { go(format: 'html') }
...@@ -612,6 +627,8 @@ describe Projects::MergeRequestsController do ...@@ -612,6 +627,8 @@ describe Projects::MergeRequestsController do
format: format format: format
end end
it_behaves_like "loads labels", :commits
context 'as html' do context 'as html' do
it 'renders the show template' do it 'renders the show template' do
go go
...@@ -630,6 +647,14 @@ describe Projects::MergeRequestsController do ...@@ -630,6 +647,14 @@ describe Projects::MergeRequestsController do
end end
end end
describe 'GET builds' do
it_behaves_like "loads labels", :builds
end
describe 'GET pipelines' do
it_behaves_like "loads labels", :pipelines
end
describe 'GET conflicts' do describe 'GET conflicts' do
let(:json_response) { JSON.parse(response.body) } let(:json_response) { JSON.parse(response.body) }
......
...@@ -46,7 +46,7 @@ describe 'Issue Boards new issue', feature: true, js: true do ...@@ -46,7 +46,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
click_button 'Cancel' click_button 'Cancel'
expect(page).to have_selector('.board-new-issue-form', visible: false) expect(page).not_to have_selector('.board-new-issue-form')
end end
end end
......
...@@ -182,6 +182,20 @@ feature 'Expand and collapse diffs', js: true, feature: true do ...@@ -182,6 +182,20 @@ feature 'Expand and collapse diffs', js: true, feature: true do
end end
end end
end end
context 'expanding a diff when symlink was converted to a regular file' do
let(:branch) { 'symlink-expand-diff' }
it 'shows the content of the regular file' do
expect(page).to have_content('This diff is collapsed')
expect(page).to have_no_content('No longer a symlink')
find('.click-to-expand').click
wait_for_ajax
expect(page).to have_content('No longer a symlink')
end
end
end end
context 'visiting a commit without collapsed diffs' do context 'visiting a commit without collapsed diffs' do
......
...@@ -80,7 +80,7 @@ feature 'Group', feature: true do ...@@ -80,7 +80,7 @@ feature 'Group', feature: true do
visit path visit path
expect(page).to have_css('.description > p > strong') expect(page).to have_css('.group-home-desc > p > strong')
end end
it 'passes through html-pipeline' do it 'passes through html-pipeline' do
...@@ -88,7 +88,7 @@ feature 'Group', feature: true do ...@@ -88,7 +88,7 @@ feature 'Group', feature: true do
visit path visit path
expect(page).to have_css('.description > p > img') expect(page).to have_css('.group-home-desc > p > img')
end end
it 'sanitizes unwanted tags' do it 'sanitizes unwanted tags' do
...@@ -96,7 +96,7 @@ feature 'Group', feature: true do ...@@ -96,7 +96,7 @@ feature 'Group', feature: true do
visit path visit path
expect(page).not_to have_css('.description h1') expect(page).not_to have_css('.group-home-desc h1')
end end
it 'permits `rel` attribute on links' do it 'permits `rel` attribute on links' do
...@@ -104,7 +104,7 @@ feature 'Group', feature: true do ...@@ -104,7 +104,7 @@ feature 'Group', feature: true do
visit path visit path
expect(page).to have_css('.description a[rel]') expect(page).to have_css('.group-home-desc a[rel]')
end end
end end
end end
...@@ -69,8 +69,6 @@ feature 'Diff notes resolve', feature: true, js: true do ...@@ -69,8 +69,6 @@ feature 'Diff notes resolve', feature: true, js: true do
page.within '.diff-content .note' do page.within '.diff-content .note' do
expect(page).to have_selector('.line-resolve-btn.is-active') expect(page).to have_selector('.line-resolve-btn.is-active')
expect(find('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}")
end end
page.within '.line-resolve-all-container' do page.within '.line-resolve-all-container' do
......
require 'spec_helper'
describe Gitlab::SidekiqThrottler do
before do
Sidekiq.options[:concurrency] = 35
stub_application_setting(
sidekiq_throttling_enabled: true,
sidekiq_throttling_factor: 0.1,
sidekiq_throttling_queues: %w[build project_cache]
)
end
describe '#execute!' do
it 'sets limits on the selected queues' do
Gitlab::SidekiqThrottler.execute!
expect(Sidekiq::Queue['build'].limit).to eq 4
expect(Sidekiq::Queue['project_cache'].limit).to eq 4
end
it 'does not set limits on other queues' do
Gitlab::SidekiqThrottler.execute!
expect(Sidekiq::Queue['merge'].limit).to be_nil
end
end
end
...@@ -113,6 +113,26 @@ describe Repository, models: true do ...@@ -113,6 +113,26 @@ describe Repository, models: true do
end end
end end
describe '#ref_exists?' do
context 'when ref exists' do
it 'returns true' do
expect(repository.ref_exists?('refs/heads/master')).to be true
end
end
context 'when ref does not exist' do
it 'returns false' do
expect(repository.ref_exists?('refs/heads/non-existent')).to be false
end
end
context 'when ref format is incorrect' do
it 'returns false' do
expect(repository.ref_exists?('refs/heads/invalid:master')).to be false
end
end
end
describe '#last_commit_for_path' do describe '#last_commit_for_path' do
subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id } subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id }
...@@ -197,6 +217,35 @@ describe Repository, models: true do ...@@ -197,6 +217,35 @@ describe Repository, models: true do
end end
end end
describe '#commit' do
context 'when ref exists' do
it 'returns commit object' do
expect(repository.commit('master'))
.to be_an_instance_of Commit
end
end
context 'when ref does not exist' do
it 'returns nil' do
expect(repository.commit('non-existent-ref')).to be_nil
end
end
context 'when ref is not valid' do
context 'when preceding tree element exists' do
it 'returns nil' do
expect(repository.commit('master:ref')).to be_nil
end
end
context 'when preceding tree element does not exist' do
it 'returns nil' do
expect(repository.commit('non-existent:ref')).to be_nil
end
end
end
end
describe "#commit_dir" do describe "#commit_dir" do
it "commits a change that creates a new directory" do it "commits a change that creates a new directory" do
expect do expect do
......
...@@ -68,6 +68,24 @@ describe API::API, api: true do ...@@ -68,6 +68,24 @@ describe API::API, api: true do
end end
end end
describe 'GET /groups/owned' do
context 'when unauthenticated' do
it 'returns authentication error' do
get api('/groups/owned')
expect(response).to have_http_status(401)
end
end
context 'when authenticated as group owner' do
it 'returns an array of groups the user owns' do
get api('/groups/owned', user2)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(group2.name)
end
end
end
describe "GET /groups/:id" do describe "GET /groups/:id" do
context "when authenticated as user" do context "when authenticated as user" do
it "returns one of user1's groups" do it "returns one of user1's groups" do
......
...@@ -123,6 +123,15 @@ describe API::API, api: true do ...@@ -123,6 +123,15 @@ describe API::API, api: true do
expect(json_response['title']).to eq('updated title') expect(json_response['title']).to eq('updated title')
end end
it 'removes a due date if nil is passed' do
milestone.update!(due_date: "2016-08-05")
put api("/projects/#{project.id}/milestones/#{milestone.id}", user), due_date: nil
expect(response).to have_http_status(200)
expect(json_response['due_date']).to be_nil
end
it 'returns a 404 error if milestone id not found' do it 'returns a 404 error if milestone id not found' do
put api("/projects/#{project.id}/milestones/1234", user), put api("/projects/#{project.id}/milestones/1234", user),
title: 'updated title' title: 'updated title'
......
...@@ -175,6 +175,30 @@ describe API::API, api: true do ...@@ -175,6 +175,30 @@ describe API::API, api: true do
end end
end end
describe 'GET /projects/owned' do
before do
project3
project4
end
context 'when unauthenticated' do
it 'returns authentication error' do
get api('/projects/owned')
expect(response).to have_http_status(401)
end
end
context 'when authenticated as project owner' do
it 'returns an array of projects the user owns' do
get api('/projects/owned', user4)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(project4.name)
expect(json_response.first['owner']['username']).to eq(user4.username)
end
end
end
describe 'GET /projects/visible' do describe 'GET /projects/visible' do
let(:public_project) { create(:project, :public) } let(:public_project) { create(:project, :public) }
...@@ -336,6 +360,14 @@ describe API::API, api: true do ...@@ -336,6 +360,14 @@ describe API::API, api: true do
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
end end
it 'sets a project as allowing merge if only_allow_merge_if_all_discussions_are_resolved is nil' do
project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: nil)
post api('/projects', user), project
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey
end
it 'sets a project as allowing merge only if all discussions are resolved' do it 'sets a project as allowing merge only if all discussions are resolved' do
project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true }) project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true })
......
...@@ -226,7 +226,7 @@ describe API::Runners, api: true do ...@@ -226,7 +226,7 @@ describe API::Runners, api: true do
context 'authorized user' do context 'authorized user' do
context 'when runner is shared' do context 'when runner is shared' do
it 'does not update runner' do it 'does not update runner' do
put api("/runners/#{shared_runner.id}", user) put api("/runners/#{shared_runner.id}", user), description: 'test'
expect(response).to have_http_status(403) expect(response).to have_http_status(403)
end end
...@@ -234,7 +234,7 @@ describe API::Runners, api: true do ...@@ -234,7 +234,7 @@ describe API::Runners, api: true do
context 'when runner is not shared' do context 'when runner is not shared' do
it 'does not update runner without access to it' do it 'does not update runner without access to it' do
put api("/runners/#{specific_runner.id}", user2) put api("/runners/#{specific_runner.id}", user2), description: 'test'
expect(response).to have_http_status(403) expect(response).to have_http_status(403)
end end
......
...@@ -67,22 +67,24 @@ describe API::API, api: true do ...@@ -67,22 +67,24 @@ describe API::API, api: true do
end end
context "when empty password" do context "when empty password" do
it "returns authentication error" do it "returns authentication error with email" do
post api("/session"), email: user.email post api("/session"), email: user.email
expect(response).to have_http_status(401)
expect(json_response['email']).to be_nil expect(response).to have_http_status(400)
expect(json_response['private_token']).to be_nil end
it "returns authentication error with username" do
post api("/session"), email: user.username
expect(response).to have_http_status(400)
end end
end end
context "when empty name" do context "when empty name" do
it "returns authentication error" do it "returns authentication error" do
post api("/session"), password: user.password post api("/session"), password: user.password
expect(response).to have_http_status(401)
expect(json_response['email']).to be_nil expect(response).to have_http_status(400)
expect(json_response['private_token']).to be_nil
end end
end end
end end
......
...@@ -68,7 +68,7 @@ describe API::API do ...@@ -68,7 +68,7 @@ describe API::API do
it 'validates variables to be a hash' do it 'validates variables to be a hash' do
post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master') post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: 'value', ref: 'master')
expect(response).to have_http_status(400) expect(response).to have_http_status(400)
expect(json_response['message']).to eq('variables needs to be a hash') expect(json_response['error']).to eq('variables is invalid')
end end
it 'validates variables needs to be a map of key-valued strings' do it 'validates variables needs to be a map of key-valued strings' do
......
...@@ -23,6 +23,7 @@ module TestEnv ...@@ -23,6 +23,7 @@ module TestEnv
'binary-encoding' => '7b1cf43', 'binary-encoding' => '7b1cf43',
'gitattributes' => '5a62481', 'gitattributes' => '5a62481',
'expand-collapse-diffs' => '4842455', 'expand-collapse-diffs' => '4842455',
'symlink-expand-diff' => '81e6355',
'expand-collapse-files' => '025db92', 'expand-collapse-files' => '025db92',
'expand-collapse-lines' => '238e82d', 'expand-collapse-lines' => '238e82d',
'video' => '8879059', 'video' => '8879059',
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
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