Commit 1ae19e1e authored by Robert Speicher's avatar Robert Speicher

Merge remote-tracking branch 'origin/8-17-stable' into 8-17-stable

parents 79b56c8a 52ade20e
...@@ -16,6 +16,7 @@ require('./components/board'); ...@@ -16,6 +16,7 @@ require('./components/board');
require('./components/board_sidebar'); require('./components/board_sidebar');
require('./components/new_list_dropdown'); require('./components/new_list_dropdown');
require('./components/modal/index'); require('./components/modal/index');
const backlogHelp = require('./components/boards_backlog_help');
require('../vue_shared/vue_resource_interceptor'); require('../vue_shared/vue_resource_interceptor');
$(() => { $(() => {
...@@ -37,6 +38,7 @@ $(() => { ...@@ -37,6 +38,7 @@ $(() => {
'board': gl.issueBoards.Board, 'board': gl.issueBoards.Board,
'board-sidebar': gl.issueBoards.BoardSidebar, 'board-sidebar': gl.issueBoards.BoardSidebar,
'board-add-issues-modal': gl.issueBoards.IssuesModal, 'board-add-issues-modal': gl.issueBoards.IssuesModal,
backlogHelp,
}, },
data: { data: {
state: Store.state, state: Store.state,
...@@ -53,6 +55,11 @@ $(() => { ...@@ -53,6 +55,11 @@ $(() => {
detailIssueVisible () { detailIssueVisible () {
return Object.keys(this.detailIssue.issue).length; return Object.keys(this.detailIssue.issue).length;
}, },
hideHelp() {
if (this.loading) return false;
return !this.state.helpHidden;
},
}, },
created () { created () {
gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId);
...@@ -104,7 +111,7 @@ $(() => { ...@@ -104,7 +111,7 @@ $(() => {
class="btn btn-create pull-right prepend-left-10 has-tooltip" class="btn btn-create pull-right prepend-left-10 has-tooltip"
type="button" type="button"
:disabled="disabled" :disabled="disabled"
@click="toggleModal(true)"> @click="toggleModal(true, false)">
Add issues Add issues
</button> </button>
`, `,
......
const Vue = require('vue');
const checkmarkIcon = require('../icons/checkmark');
const Store = gl.issueBoards.BoardsStore;
const ModalStore = gl.issueBoards.ModalStore;
module.exports = Vue.extend({
mixins: [gl.issueBoards.ModalMixins],
data() {
return ModalStore.store;
},
computed: {
disabled() {
return !Store.state.lists
.filter(list => list.type !== 'blank' && list.type !== 'done').length;
},
},
template: `
<div class="boards-backlog-help text-center">
<h4>
We moved the Backlog
<button
type="button"
class="close"
aria-label="Close backlog help"
@click="toggleModal(false)">
<i
class="fa fa-times"
aria-hidden="true">
</i>
</button>
</h4>
<div class="backlog-help-icon">${checkmarkIcon}</div>
<p>
<a href="http://docs.gitlab.com/ce/user/project/issue_board.html">Read the docs</a> for more details
</p>
<p>
Populate the board using this button
</p>
<div class="text-center">
<button
class="btn btn-success"
type="button"
:disabled="disabled"
@click="toggleModal(true, false)">
Add issues
</button>
</div>
</div>
`,
});
...@@ -74,7 +74,7 @@ require('./lists_dropdown'); ...@@ -74,7 +74,7 @@ require('./lists_dropdown');
<button <button
class="btn btn-default pull-right" class="btn btn-default pull-right"
type="button" type="button"
@click="toggleModal(false)"> @click="toggleModal(false, false)">
Cancel Cancel
</button> </button>
</footer> </footer>
......
...@@ -57,7 +57,7 @@ const modalFilters = require('./filters'); ...@@ -57,7 +57,7 @@ const modalFilters = require('./filters');
class="close" class="close"
data-dismiss="modal" data-dismiss="modal"
aria-label="Close" aria-label="Close"
@click="toggleModal(false)"> @click="toggleModal(false, false)">
<span aria-hidden="true">×</span> <span aria-hidden="true">×</span>
</button> </button>
</h2> </h2>
......
/* global Vue */ /* global Vue */
/* global dateFormat */
Vue.filter('due-date', (value) => { Vue.filter('due-date', (value) => {
const date = new Date(value); const date = new Date(value);
return $.datepicker.formatDate('M d, yy', date); return dateFormat(date, 'mmm d, yyyy', true);
}); });
module.exports = '<svg viewBox="0 0 28 22" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="m45.4978994 24.9892046c0 .4726429-.1654225.8743833-.4962726 1.2052333l-15.2426565 15.2426565c-.33085.33085-.7325904.4962725-1.2052333.4962725-.4726428 0-.8743833-.1654225-1.2052333-.4962725l-8.8265615-8.8265616c-.33085-.33085-.4962726-.7325904-.4962726-1.2052333 0-.4726428.1654226-.8743833.4962726-1.2052333l2.4104666-2.4104666c.33085-.33085.7325904-.4962725 1.2052333-.4962725s.8743833.1654225 1.2052333.4962725l5.2108616 5.2285857 11.6269566-11.6446806c.33085-.33085.7325905-.4962726 1.2052333-.4962726.4726429 0 .8743833.1654226 1.2052333.4962726l2.4104666 2.4104666c.3308501.33085.4962726.7325904.4962726 1.2052333z"/><mask id="b" fill="#fff" height="21.0561348" width="27.4722297" x="0" y="0"><use xlink:href="#a"/></mask></defs><use fill="none" mask="url(#b)" stroke="#1a97d5" stroke-width="2" transform="translate(-18 -20)" xlink:href="#a"/></svg>';
/* global Cookies */
(() => { (() => {
const Store = gl.issueBoards.BoardsStore;
const ModalStore = gl.issueBoards.ModalStore; const ModalStore = gl.issueBoards.ModalStore;
gl.issueBoards.ModalMixins = { gl.issueBoards.ModalMixins = {
methods: { methods: {
toggleModal(toggle) { toggleModal(toggleModal, setCookie = true) {
ModalStore.store.showAddIssuesModal = toggle; if (setCookie) {
Cookies.set('boards_backlog_help_hidden', true);
Store.state.helpHidden = true;
}
ModalStore.store.showAddIssuesModal = toggleModal;
}, },
changeTab(tab) { changeTab(tab) {
ModalStore.store.activeTab = tab; ModalStore.store.activeTab = tab;
......
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
label_name: gl.utils.getParameterValues('label_name[]'), label_name: gl.utils.getParameterValues('label_name[]'),
search: '' search: ''
}; };
this.state.helpHidden = Cookies.get('boards_backlog_help_hidden') === 'true';
}, },
addList (listObj) { addList (listObj) {
const list = new List(listObj); const list = new List(listObj);
......
...@@ -26,7 +26,7 @@ class PipelinesStore { ...@@ -26,7 +26,7 @@ class PipelinesStore {
*/ */
startTimeAgoLoops() { startTimeAgoLoops() {
const startTimeLoops = () => { const startTimeLoops = () => {
this.timeLoopInterval = setInterval(function timeloopInterval() { this.timeLoopInterval = setInterval(() => {
this.$children[0].$children.reduce((acc, component) => { this.$children[0].$children.reduce((acc, component) => {
const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0]; const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0];
acc.push(timeAgoComponent); acc.push(timeAgoComponent);
......
...@@ -91,6 +91,9 @@ require('./lib/utils/common_utils'); ...@@ -91,6 +91,9 @@ require('./lib/utils/common_utils');
}, },
}, },
SanitizationFilter: { SanitizationFilter: {
'a[name]:not([href]):empty'(el, text) {
return el.outerHTML;
},
'dl'(el, text) { 'dl'(el, text) {
let lines = text.trim().split('\n'); let lines = text.trim().split('\n');
// Add two spaces to the front of subsequent list items lines, // Add two spaces to the front of subsequent list items lines,
......
/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, max-len */ /* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, max-len */
/* global Vue */
/* global CommentsStore */ /* global CommentsStore */
const Vue = require('vue');
(() => { (() => {
const CommentAndResolveBtn = Vue.extend({ const CommentAndResolveBtn = Vue.extend({
...@@ -9,13 +9,11 @@ ...@@ -9,13 +9,11 @@
}, },
data() { data() {
return { return {
textareaIsEmpty: true textareaIsEmpty: true,
discussion: {},
}; };
}, },
computed: { computed: {
discussion: function () {
return CommentsStore.state[this.discussionId];
},
showButton: function () { showButton: function () {
if (this.discussion) { if (this.discussion) {
return this.discussion.isResolvable(); return this.discussion.isResolvable();
...@@ -42,6 +40,9 @@ ...@@ -42,6 +40,9 @@
} }
} }
}, },
created() {
this.discussion = CommentsStore.state[this.discussionId];
},
mounted: 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() === '';
......
/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, space-before-function-paren, no-lonely-if, no-continue, brace-style, max-len, quotes */ /* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, space-before-function-paren, no-lonely-if, no-continue, brace-style, max-len, quotes */
/* global Vue */
/* global DiscussionMixins */ /* global DiscussionMixins */
/* global CommentsStore */ /* global CommentsStore */
const Vue = require('vue');
(() => { (() => {
const JumpToDiscussion = Vue.extend({ const JumpToDiscussion = Vue.extend({
...@@ -12,12 +12,10 @@ ...@@ -12,12 +12,10 @@
data: function () { data: function () {
return { return {
discussions: CommentsStore.state, discussions: CommentsStore.state,
discussion: {},
}; };
}, },
computed: { computed: {
discussion: function () {
return this.discussions[this.discussionId];
},
allResolved: function () { allResolved: function () {
return this.unresolvedDiscussionCount === 0; return this.unresolvedDiscussionCount === 0;
}, },
...@@ -186,7 +184,10 @@ ...@@ -186,7 +184,10 @@
offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()) offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight())
}); });
} }
} },
created() {
this.discussion = this.discussions[this.discussionId];
},
}); });
Vue.component('jump-to-discussion', JumpToDiscussion); Vue.component('jump-to-discussion', JumpToDiscussion);
......
/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, no-new, max-len */ /* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, no-new, max-len */
/* global Vue */
/* global CommentsStore */ /* global CommentsStore */
/* global ResolveService */ /* global ResolveService */
/* global Flash */ /* global Flash */
const Vue = require('vue');
(() => { (() => {
const ResolveBtn = Vue.extend({ const ResolveBtn = Vue.extend({
...@@ -10,14 +10,14 @@ ...@@ -10,14 +10,14 @@
noteId: Number, noteId: Number,
discussionId: String, discussionId: String,
resolved: Boolean, resolved: Boolean,
projectPath: String,
canResolve: Boolean, canResolve: Boolean,
resolvedBy: String resolvedBy: String
}, },
data: function () { data: function () {
return { return {
discussions: CommentsStore.state, discussions: CommentsStore.state,
loading: false loading: false,
note: {},
}; };
}, },
watch: { watch: {
...@@ -30,13 +30,6 @@ ...@@ -30,13 +30,6 @@
discussion: function () { discussion: function () {
return this.discussions[this.discussionId]; return this.discussions[this.discussionId];
}, },
note: function () {
if (this.discussion) {
return this.discussion.getNote(this.noteId);
} else {
return undefined;
}
},
buttonText: function () { buttonText: function () {
if (this.isResolved) { if (this.isResolved) {
return `Resolved by ${this.resolvedByName}`; return `Resolved by ${this.resolvedByName}`;
...@@ -73,10 +66,10 @@ ...@@ -73,10 +66,10 @@
if (this.isResolved) { if (this.isResolved) {
promise = ResolveService promise = ResolveService
.unresolve(this.projectPath, this.noteId); .unresolve(this.noteId);
} else { } else {
promise = ResolveService promise = ResolveService
.resolve(this.projectPath, this.noteId); .resolve(this.noteId);
} }
promise.then((response) => { promise.then((response) => {
...@@ -106,6 +99,8 @@ ...@@ -106,6 +99,8 @@
}, },
created: function () { created: function () {
CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy); CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy);
this.note = this.discussion.getNote(this.noteId);
} }
}); });
......
/* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */ /* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */
/* global Vue */
/* global DiscussionMixins */ /* global DiscussionMixins */
/* global CommentsStore */ /* global CommentsStore */
const Vue = require('vue');
((w) => { ((w) => {
w.ResolveCount = Vue.extend({ w.ResolveCount = Vue.extend({
......
/* eslint-disable object-shorthand, func-names, space-before-function-paren, comma-dangle, no-else-return, quotes, max-len */ /* eslint-disable object-shorthand, func-names, space-before-function-paren, comma-dangle, no-else-return, quotes, max-len */
/* global Vue */
/* global CommentsStore */ /* global CommentsStore */
/* global ResolveService */ /* global ResolveService */
const Vue = require('vue');
(() => { (() => {
const ResolveDiscussionBtn = Vue.extend({ const ResolveDiscussionBtn = Vue.extend({
props: { props: {
discussionId: String, discussionId: String,
mergeRequestId: Number, mergeRequestId: Number,
projectPath: String,
canResolve: Boolean, canResolve: Boolean,
}, },
data: function() { data: function() {
return { return {
discussions: CommentsStore.state discussion: {},
}; };
}, },
computed: { computed: {
discussion: function () {
return this.discussions[this.discussionId];
},
showButton: function () { showButton: function () {
if (this.discussion) { if (this.discussion) {
return this.discussion.isResolvable(); return this.discussion.isResolvable();
...@@ -51,11 +48,13 @@ ...@@ -51,11 +48,13 @@
}, },
methods: { methods: {
resolve: function () { resolve: function () {
ResolveService.toggleResolveForDiscussion(this.projectPath, this.mergeRequestId, this.discussionId); ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId);
} }
}, },
created: function () { created: function () {
CommentsStore.createDiscussion(this.discussionId, this.canResolve); CommentsStore.createDiscussion(this.discussionId, this.canResolve);
this.discussion = CommentsStore.state[this.discussionId];
} }
}); });
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
/* global ResolveCount */ /* global ResolveCount */
function requireAll(context) { return context.keys().map(context); } function requireAll(context) { return context.keys().map(context); }
const Vue = require('vue');
requireAll(require.context('./models', false, /^\.\/.*\.(js|es6)$/)); requireAll(require.context('./models', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./stores', false, /^\.\/.*\.(js|es6)$/)); requireAll(require.context('./stores', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./services', false, /^\.\/.*\.(js|es6)$/)); requireAll(require.context('./services', false, /^\.\/.*\.(js|es6)$/));
...@@ -10,11 +11,14 @@ requireAll(require.context('./mixins', false, /^\.\/.*\.(js|es6)$/)); ...@@ -10,11 +11,14 @@ requireAll(require.context('./mixins', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./components', false, /^\.\/.*\.(js|es6)$/)); requireAll(require.context('./components', false, /^\.\/.*\.(js|es6)$/));
$(() => { $(() => {
const projectPath = document.querySelector('.merge-request').dataset.projectPath;
const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn'; const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn';
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.diffNoteApps = {}; window.gl.diffNoteApps = {};
window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath);
gl.diffNotesCompileComponents = () => { gl.diffNotesCompileComponents = () => {
const $components = $(COMPONENT_SELECTOR).filter(function () { const $components = $(COMPONENT_SELECTOR).filter(function () {
return $(this).closest('resolve-count').length !== 1; return $(this).closest('resolve-count').length !== 1;
......
/* eslint-disable class-methods-use-this, one-var, camelcase, no-new, comma-dangle, no-param-reassign, max-len */ /* eslint-disable class-methods-use-this, one-var, camelcase, no-new, comma-dangle, no-param-reassign, max-len */
/* global Vue */
/* global Flash */ /* global Flash */
/* global CommentsStore */ /* global CommentsStore */
((w) => { const Vue = window.Vue = require('vue');
class ResolveServiceClass { window.Vue.use(require('vue-resource'));
constructor() { require('../../vue_shared/vue_resource_interceptor');
this.noteResource = Vue.resource('notes{/noteId}/resolve');
this.discussionResource = Vue.resource('merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve');
}
setCSRF() { (() => {
Vue.http.headers.common['X-CSRF-Token'] = $.rails.csrfToken(); window.gl = window.gl || {};
}
prepareRequest(root) { class ResolveServiceClass {
this.setCSRF(); constructor(root) {
Vue.http.options.root = root; this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve`);
this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve`);
} }
resolve(projectPath, noteId) { resolve(noteId) {
this.prepareRequest(projectPath);
return this.noteResource.save({ noteId }, {}); return this.noteResource.save({ noteId }, {});
} }
unresolve(projectPath, noteId) { unresolve(noteId) {
this.prepareRequest(projectPath);
return this.noteResource.delete({ noteId }, {}); return this.noteResource.delete({ noteId }, {});
} }
toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId) { toggleResolveForDiscussion(mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId]; const discussion = CommentsStore.state[discussionId];
const isResolved = discussion.isResolved(); const isResolved = discussion.isResolved();
let promise; let promise;
if (isResolved) { if (isResolved) {
promise = this.unResolveAll(projectPath, mergeRequestId, discussionId); promise = this.unResolveAll(mergeRequestId, discussionId);
} else { } else {
promise = this.resolveAll(projectPath, mergeRequestId, discussionId); promise = this.resolveAll(mergeRequestId, discussionId);
} }
promise.then((response) => { promise.then((response) => {
...@@ -62,11 +54,9 @@ ...@@ -62,11 +54,9 @@
}); });
} }
resolveAll(projectPath, mergeRequestId, discussionId) { resolveAll(mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId]; const discussion = CommentsStore.state[discussionId];
this.prepareRequest(projectPath);
discussion.loading = true; discussion.loading = true;
return this.discussionResource.save({ return this.discussionResource.save({
...@@ -75,11 +65,9 @@ ...@@ -75,11 +65,9 @@
}, {}); }, {});
} }
unResolveAll(projectPath, mergeRequestId, discussionId) { unResolveAll(mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId]; const discussion = CommentsStore.state[discussionId];
this.prepareRequest(projectPath);
discussion.loading = true; discussion.loading = true;
return this.discussionResource.delete({ return this.discussionResource.delete({
...@@ -89,5 +77,5 @@ ...@@ -89,5 +77,5 @@
} }
} }
w.ResolveService = new ResolveServiceClass(); gl.DiffNotesResolveServiceClass = ResolveServiceClass;
})(window); })();
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
class FilteredSearchDropdown { class FilteredSearchDropdown {
constructor(droplab, dropdown, input, filter) { constructor(droplab, dropdown, input, filter) {
this.droplab = droplab; this.droplab = droplab;
this.hookId = input.getAttribute('data-id'); this.hookId = input && input.getAttribute('data-id');
this.input = input; this.input = input;
this.filter = filter; this.filter = filter;
this.dropdown = dropdown; this.dropdown = dropdown;
......
...@@ -47,9 +47,11 @@ ...@@ -47,9 +47,11 @@
} }
// Only filter asynchronously only if option remote is set // Only filter asynchronously only if option remote is set
if (this.options.remote) { if (this.options.remote) {
$inputContainer.parent().addClass('is-loading');
clearTimeout(timeout); clearTimeout(timeout);
return timeout = setTimeout(function() { return timeout = setTimeout(function() {
return this.options.query(this.input.val(), function(data) { return this.options.query(this.input.val(), function(data) {
$inputContainer.parent().removeClass('is-loading');
return this.options.callback(data); return this.options.callback(data);
}.bind(this)); }.bind(this));
}.bind(this), 250); }.bind(this), 250);
......
...@@ -154,7 +154,7 @@ require('./smart_interval'); ...@@ -154,7 +154,7 @@ require('./smart_interval');
return $.getJSON(this.opts.ci_status_url, (function(_this) { return $.getJSON(this.opts.ci_status_url, (function(_this) {
return function(data) { return function(data) {
var message, status, title; var message, status, title;
if (data.status === '') { if (!data.status) {
return; return;
} }
if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
......
...@@ -455,7 +455,7 @@ require('vendor/task_list'); ...@@ -455,7 +455,7 @@ require('vendor/task_list');
var mergeRequestId = $form.data('noteable-iid'); var mergeRequestId = $form.data('noteable-iid');
if (ResolveService != null) { if (ResolveService != null) {
ResolveService.toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId); ResolveService.toggleResolveForDiscussion(mergeRequestId, discussionId);
} }
} }
......
...@@ -14,5 +14,16 @@ ...@@ -14,5 +14,16 @@
window.addEventListener('focus', startIntervals); window.addEventListener('focus', startIntervals);
window.addEventListener('blur', removeIntervals); window.addEventListener('blur', removeIntervals);
document.addEventListener('beforeunload', removeAll); document.addEventListener('beforeunload', removeAll);
// add removeAll methods to stack
const stack = gl.VueRealtimeListener.reset;
gl.VueRealtimeListener.reset = () => {
gl.VueRealtimeListener.reset = stack;
removeAll();
stack();
};
}; };
// remove all event listeners and intervals
gl.VueRealtimeListener.reset = () => undefined; // noop
})(window.gl || (window.gl = {})); })(window.gl || (window.gl = {}));
...@@ -111,7 +111,7 @@ require('./commit'); ...@@ -111,7 +111,7 @@ require('./commit');
* If provided, returns the commit ref. * If provided, returns the commit ref.
* Needed to render the commit component column. * Needed to render the commit component column.
* *
* Matched `url` prop sent in the API to `path` prop needed * Matches `path` prop sent in the API to `ref_url` prop needed
* in the commit component. * in the commit component.
* *
* @returns {Object|Undefined} * @returns {Object|Undefined}
...@@ -119,8 +119,8 @@ require('./commit'); ...@@ -119,8 +119,8 @@ require('./commit');
commitRef() { commitRef() {
if (this.pipeline.ref) { if (this.pipeline.ref) {
return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
if (prop === 'url') { if (prop === 'path') {
accumulator.path = this.pipeline.ref[prop]; accumulator.ref_url = this.pipeline.ref[prop];
} else { } else {
accumulator[prop] = this.pipeline.ref[prop]; accumulator[prop] = this.pipeline.ref[prop];
} }
......
...@@ -128,8 +128,7 @@ ...@@ -128,8 +128,7 @@
.note-action-button .link-highlight, .note-action-button .link-highlight,
.toolbar-btn, .toolbar-btn,
.dropdown-toggle-caret, .dropdown-toggle-caret {
.fa:not(.fa-bell) {
@include transition(color); @include transition(color);
} }
......
...@@ -28,6 +28,8 @@ ...@@ -28,6 +28,8 @@
.avatar { .avatar {
@extend .avatar-circle; @extend .avatar-circle;
@include transition-property(none);
width: 40px; width: 40px;
height: 40px; height: 40px;
padding: 0; padding: 0;
......
...@@ -9,6 +9,8 @@ ...@@ -9,6 +9,8 @@
} }
.user-calendar-activities { .user-calendar-activities {
direction: ltr;
.str-truncated { .str-truncated {
max-width: 70%; max-width: 70%;
} }
......
...@@ -272,7 +272,7 @@ header { ...@@ -272,7 +272,7 @@ header {
.header-user { .header-user {
.dropdown-menu-nav { .dropdown-menu-nav {
width: 140px; min-width: 140px;
margin-top: -5px; margin-top: -5px;
} }
} }
......
...@@ -138,6 +138,13 @@ pre { ...@@ -138,6 +138,13 @@ pre {
margin: 0; margin: 0;
} }
blockquote {
color: $gl-grayish-blue;
padding: 0 0 0 15px;
margin: 0;
border-left: 3px solid $white-dark;
}
span.highlight_word { span.highlight_word {
background-color: $highlighted-highlight-word !important; background-color: $highlighted-highlight-word !important;
} }
......
...@@ -522,3 +522,39 @@ ...@@ -522,3 +522,39 @@
} }
} }
} }
.boards-backlog-help {
display: inline-block;
width: 260px;
padding: 15px;
border: 1px solid $border-color;
border-radius: 2px;
white-space: normal;
> h4 {
margin-top: 0;
font-size: 14px;
}
.close {
padding: 2px 0;
font-size: 12px;
line-height: 0;
}
}
.backlog-help-icon {
display: -webkit-flex;
display: flex;
margin: 15px auto;
width: 60px;
height: 60px;
border: 1px solid $blue-normal;
border-radius: 50%;
> svg {
width: 35px;
margin-left: auto;
margin-right: auto;
}
}
...@@ -85,24 +85,21 @@ ...@@ -85,24 +85,21 @@
-webkit-align-items: center; -webkit-align-items: center;
align-items: center; align-items: center;
i,
svg {
margin-right: 8px;
}
svg { svg {
margin-right: 4px;
position: relative; position: relative;
top: 1px; top: 1px;
overflow: visible; overflow: visible;
} }
&> span { & > span {
padding-right: 4px; padding-right: 4px;
} }
&.ci-success_with_warnings {
i {
color: $gl-warning;
}
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
flex-wrap: wrap; flex-wrap: wrap;
} }
...@@ -125,6 +122,12 @@ ...@@ -125,6 +122,12 @@
line-height: 16px; line-height: 16px;
} }
@media (min-width: $screen-sm-min) {
.stage-cell {
padding: 0 4px;
}
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
order: 1; order: 1;
margin-top: $gl-padding-top; margin-top: $gl-padding-top;
......
...@@ -12,7 +12,6 @@ class ApplicationController < ActionController::Base ...@@ -12,7 +12,6 @@ class ApplicationController < ActionController::Base
before_action :authenticate_user_from_private_token! before_action :authenticate_user_from_private_token!
before_action :authenticate_user! before_action :authenticate_user!
before_action :validate_user_service_ticket! before_action :validate_user_service_ticket!
before_action :reject_blocked!
before_action :check_password_expiration before_action :check_password_expiration
before_action :check_2fa_requirement before_action :check_2fa_requirement
before_action :ldap_security_check before_action :ldap_security_check
...@@ -87,22 +86,8 @@ class ApplicationController < ActionController::Base ...@@ -87,22 +86,8 @@ class ApplicationController < ActionController::Base
logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}" logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}"
end end
def reject_blocked!
if current_user && current_user.blocked?
sign_out current_user
flash[:alert] = "Your account is blocked. Retry when an admin has unblocked it."
redirect_to new_user_session_path
end
end
def after_sign_in_path_for(resource) def after_sign_in_path_for(resource)
if resource.is_a?(User) && resource.respond_to?(:blocked?) && resource.blocked? stored_location_for(:redirect) || stored_location_for(resource) || root_path
sign_out resource
flash[:alert] = "Your account is blocked. Retry when an admin has unblocked it."
new_user_session_path
else
stored_location_for(:redirect) || stored_location_for(resource) || root_path
end
end end
def after_sign_out_path_for(resource) def after_sign_out_path_for(resource)
......
class Dashboard::ProjectsController < Dashboard::ApplicationController class Dashboard::ProjectsController < Dashboard::ApplicationController
include FilterProjects include FilterProjects
before_action :event_filter
def index def index
@projects = current_user.authorized_projects.sorted_by_activity @projects = load_projects(current_user.authorized_projects)
@projects = filter_projects(@projects)
@projects = @projects.includes(:namespace)
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]) @projects = @projects.page(params[:page])
respond_to do |format| respond_to do |format|
format.html { @last_push = current_user.recent_push } format.html { @last_push = current_user.recent_push }
format.atom do format.atom do
event_filter
load_events load_events
render layout: false render layout: false
end end
...@@ -26,9 +21,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -26,9 +21,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end end
def starred def starred
@projects = current_user.viewable_starred_projects.sorted_by_activity @projects = load_projects(current_user.viewable_starred_projects)
@projects = filter_projects(@projects) @projects = @projects.includes(:forked_from_project, :tags)
@projects = @projects.includes(:namespace, :forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort]) @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]) @projects = @projects.page(params[:page])
...@@ -37,7 +31,6 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -37,7 +31,6 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
render json: { render json: {
html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects }) html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
...@@ -48,9 +41,15 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -48,9 +41,15 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
private private
def load_projects(base_scope)
projects = base_scope.sorted_by_activity.includes(:namespace)
filter_projects(projects)
end
def load_events def load_events
@events = Event.in_projects(@projects) @events = Event.in_projects(load_projects(current_user.authorized_projects))
@events = @event_filter.apply_filter(@events).with_associations @events = event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0) @events = @events.limit(20).offset(params[:offset] || 0)
end end
end end
class Explore::ApplicationController < ApplicationController class Explore::ApplicationController < ApplicationController
skip_before_action :authenticate_user!, :reject_blocked! skip_before_action :authenticate_user!
layout 'explore' layout 'explore'
end end
class HelpController < ApplicationController class HelpController < ApplicationController
skip_before_action :authenticate_user!, :reject_blocked! skip_before_action :authenticate_user!
layout 'help' layout 'help'
......
class KodingController < ApplicationController class KodingController < ApplicationController
before_action :check_integration!, :authenticate_user!, :reject_blocked! before_action :check_integration!
layout 'koding' layout 'koding'
def index def index
......
...@@ -48,6 +48,10 @@ class Projects::LfsApiController < Projects::GitHttpClientController ...@@ -48,6 +48,10 @@ class Projects::LfsApiController < Projects::GitHttpClientController
objects.each do |object| objects.each do |object|
if existing_oids.include?(object[:oid]) if existing_oids.include?(object[:oid])
object[:actions] = download_actions(object) object[:actions] = download_actions(object)
if Guest.can?(:download_code, project)
object[:authenticated] = true
end
else else
object[:error] = { object[:error] = {
code: 404, code: 404,
......
class Projects::UploadsController < Projects::ApplicationController class Projects::UploadsController < Projects::ApplicationController
skip_before_action :reject_blocked!, :project, skip_before_action :project, :repository,
:repository, if: -> { action_name == 'show' && image_or_video? } if: -> { action_name == 'show' && image_or_video? }
before_action :authorize_upload_file!, only: [:create] before_action :authorize_upload_file!, only: [:create]
......
class SearchController < ApplicationController class SearchController < ApplicationController
skip_before_action :authenticate_user!, :reject_blocked! skip_before_action :authenticate_user!
include SearchHelper include SearchHelper
......
module BuildsHelper module BuildsHelper
def sidebar_build_class(build, current_build) def sidebar_build_class(build, current_build)
build_class = '' build_class = ''
build_class += ' active' if build == current_build build_class += ' active' if build.id === current_build.id
build_class += ' retried' if build.retried? build_class += ' retried' if build.retried?
build_class build_class
end end
......
...@@ -36,10 +36,10 @@ class Event < ActiveRecord::Base ...@@ -36,10 +36,10 @@ class Event < ActiveRecord::Base
scope :code_push, -> { where(action: PUSHED) } scope :code_push, -> { where(action: PUSHED) }
scope :in_projects, ->(projects) do scope :in_projects, ->(projects) do
where(project_id: projects.map(&:id)).recent where(project_id: projects).recent
end end
scope :with_associations, -> { includes(project: :namespace) } scope :with_associations, -> { includes(:author, :project, project: :namespace).preload(:target) }
scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) } scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) }
class << self class << self
......
...@@ -1230,6 +1230,14 @@ class Repository ...@@ -1230,6 +1230,14 @@ class Repository
action[:content] action[:content]
end end
detect = CharlockHolmes::EncodingDetector.new.detect(content) if content
unless detect && detect[:type] == :binary
# When writing to the repo directly as we are doing here,
# the `core.autocrlf` config isn't taken into account.
content.gsub!("\r\n", "\n") if self.autocrlf
end
oid = rugged.write(content, :blob) oid = rugged.write(content, :blob)
index.add(path: path, oid: oid, mode: mode) index.add(path: path, oid: oid, mode: mode)
......
...@@ -167,6 +167,15 @@ class User < ActiveRecord::Base ...@@ -167,6 +167,15 @@ class User < ActiveRecord::Base
def blocked? def blocked?
true true
end end
def active_for_authentication?
false
end
def inactive_message
"Your account has been blocked. Please contact your GitLab " \
"administrator if you think this is an error."
end
end end
end end
......
- if discussion.for_merge_request? - if discussion.for_merge_request?
%resolve-discussion-btn{ ":project-path" => "'#{project_path(discussion.project)}'", %resolve-discussion-btn{ ":discussion-id" => "'#{discussion.id}'",
":discussion-id" => "'#{discussion.id}'",
":merge-request-id" => discussion.noteable.iid, ":merge-request-id" => discussion.noteable.iid,
":can-resolve" => discussion.can_resolve?(current_user), ":can-resolve" => discussion.can_resolve?(current_user),
"inline-template" => true } "inline-template" => true }
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
.boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" } .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
.boards-app-loading.text-center{ "v-if" => "loading" } .boards-app-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin") = icon("spinner spin")
%backlog-help{ "v-if" => "hideHelp" }
%board{ "v-cloak" => true, %board{ "v-cloak" => true,
"v-for" => "list in state.lists", "v-for" => "list in state.lists",
"ref" => "board", "ref" => "board",
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
- else - else
%span.api.monospace API %span.api.monospace API
- if pipeline.latest? - if pipeline.latest?
%span.label.label-success.has-tooltip{ title: 'Latest job for this branch' } latest %span.label.label-success.has-tooltip{ title: 'Latest pipeline for this branch' } latest
- if pipeline.triggered? - if pipeline.triggered?
%span.label.label-primary triggered %span.label.label-primary triggered
- if pipeline.yaml_errors.present? - if pipeline.yaml_errors.present?
...@@ -61,7 +61,7 @@ ...@@ -61,7 +61,7 @@
.btn-group.inline .btn-group.inline
- if actions.any? - if actions.any?
.btn-group .btn-group
%button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual job', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual job' } %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual pipeline', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual pipeline' }
= custom_icon('icon_play') = custom_icon('icon_play')
= icon('caret-down', 'aria-hidden' => 'true') = icon('caret-down', 'aria-hidden' => 'true')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
......
...@@ -10,13 +10,13 @@ ...@@ -10,13 +10,13 @@
- if diff_file.renamed_file - if diff_file.renamed_file
- old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) - old_path, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
%strong %strong.file-title-name.has-tooltip{ data: { title: old_path, container: 'body' } }
= old_path = old_path
&rarr; &rarr;
%strong %strong.file-title-name.has-tooltip{ data: { title: new_path, container: 'body' } }
= new_path = new_path
- else - else
%strong %strong.file-title-name.has-tooltip{ data: { title: diff_file.new_path, container: 'body' } }
= diff_file.new_path = diff_file.new_path
- if diff_file.deleted_file - if diff_file.deleted_file
deleted deleted
......
...@@ -3,10 +3,9 @@ ...@@ -3,10 +3,9 @@
- page_description @merge_request.description - page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes - page_card_attributes @merge_request.card_attributes
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('lib_vue')
= page_specific_javascript_bundle_tag('diff_notes') = page_specific_javascript_bundle_tag('diff_notes')
.merge-request{ 'data-url' => merge_request_path(@merge_request) } .merge-request{ 'data-url' => merge_request_path(@merge_request), 'data-project-path' => project_path(@merge_request.project) }
= render "projects/merge_requests/show/mr_title" = render "projects/merge_requests/show/mr_title"
.merge-request-details.issuable-details{ data: { id: @merge_request.project.id } } .merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
......
...@@ -9,8 +9,9 @@ ...@@ -9,8 +9,9 @@
Pipeline Pipeline
= link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline' = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline'
= ci_label_for_status(status) = ci_label_for_status(status)
.mr-widget-pipeline-graph - if @pipeline.stages.any?
= render 'shared/mini_pipeline_graph', pipeline: @pipeline, klass: 'js-pipeline-inline-mr-widget-graph' .mr-widget-pipeline-graph
= render 'shared/mini_pipeline_graph', pipeline: @pipeline, klass: 'js-pipeline-inline-mr-widget-graph'
%span %span
for for
= succeed "." do = succeed "." do
......
...@@ -16,13 +16,13 @@ ...@@ -16,13 +16,13 @@
gitlab_icon: "#{asset_path 'gitlab_logo.png'}", gitlab_icon: "#{asset_path 'gitlab_logo.png'}",
ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}", ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}",
ci_message: { ci_message: {
normal: "Job {{status}} for \"{{title}}\"", normal: "Pipeline {{status}} for \"{{title}}\"",
preparing: "{{status}} job for \"{{title}}\"" preparing: "{{status}} pipeline for \"{{title}}\""
}, },
ci_enable: #{@project.ci_service ? "true" : "false"}, ci_enable: #{@project.ci_service ? "true" : "false"},
ci_title: { ci_title: {
preparing: "{{status}} job", preparing: "{{status}} pipeline",
normal: "Job {{status}}" normal: "Pipeline {{status}}"
}, },
ci_sha: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.short_sha : ''}", ci_sha: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.short_sha : ''}",
ci_pipeline: #{@merge_request.head_pipeline.try(:id).to_json}, ci_pipeline: #{@merge_request.head_pipeline.try(:id).to_json},
......
%h4 %h4
= icon('exclamation-triangle') = icon('exclamation-triangle')
The job for this merge request failed The pipeline for this merge request failed
%p %p
Please retry the job or push a new commit to fix the failure. Please retry the job or push a new commit to fix the failure.
...@@ -30,8 +30,7 @@ ...@@ -30,8 +30,7 @@
- if note.resolvable? - if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note) - can_resolve = can?(current_user, :resolve_note, note)
%resolve-btn{ "project-path" => "#{project_path(note.project)}", %resolve-btn{ "discussion-id" => "#{note.discussion_id}",
"discussion-id" => "#{note.discussion_id}",
":note-id" => note.id, ":note-id" => note.id,
":resolved" => note.resolved?, ":resolved" => note.resolved?,
":can-resolve" => can_resolve, ":can-resolve" => can_resolve,
......
...@@ -65,7 +65,7 @@ ...@@ -65,7 +65,7 @@
In the In the
%code .gitlab-ci.yml %code .gitlab-ci.yml
of another project, include the following snippet. of another project, include the following snippet.
The project will be rebuilt at the end of the job. The project will be rebuilt at the end of the pipeline.
%pre %pre
:plain :plain
...@@ -89,7 +89,7 @@ ...@@ -89,7 +89,7 @@
%p.light %p.light
Add Add
%code variables[VARIABLE]=VALUE %code variables[VARIABLE]=VALUE
to an API request. Variable values can be used to distinguish between triggered jobs and normal jobs. to an API request. Variable values can be used to distinguish between triggered pipelines and normal pipelines.
With cURL: With cURL:
......
---
title: Include :author, :project, and :target in Event.with_associations
merge_request:
author:
---
title: Don't instantiate AR objects in Event.in_projects
merge_request:
author:
---
title: Update doc for enabling or disabling GitLab CI
merge_request: 8965
author: Takuya Noguchi
---
title: Fixes Pipelines table is not showing branch name for commit
merge_request:
author:
---
title: Fix current build arrow indicator
merge_request:
author:
---
title: Fix contribution activity alignment
merge_request:
author:
---
title: Add space between text and loading icon in Megre Request Widget
merge_request: 9119
author:
---
title: Show Pipeline(not Job) in MR desktop notification
merge_request:
author:
---
title: Display loading indicator when filtering ref switcher dropdown
merge_request:
author:
---
title: Show pipeline graph in MR widget if there are any stages
merge_request:
author:
---
title: Fix icon colors in merge request widget mini graph
merge_request:
author:
---
title: Improve blockquote formatting in notification emails
merge_request:
author:
---
title: Adds container to tooltip in order to make it work with overflow:hidden in
parent element
merge_request:
author:
---
title: Don't connect in Gitlab::Database.adapter_name
merge_request:
author:
---
title: Fix job to pipeline renaming
merge_request: 9147
author:
---
title: Fix timezone on issue boards due date
merge_request:
author:
---
title: Support unauthenticated LFS object downloads for public projects
merge_request: 8824
author: Ben Boeckel
---
title: Don't perform Devise trackable updates on blocked User records
merge_request: 8915
author:
---
title: Add index to ci_trigger_requests for commit_id
merge_request:
author:
---
title: Add indices to improve loading of labels page
merge_request:
author:
---
title: upgrade babel 5.8.x to babel 6.22.x
merge_request: 9072
author:
---
title: upgrade to webpack v2.2
merge_request: 9078
author:
...@@ -302,3 +302,21 @@ ...@@ -302,3 +302,21 @@
:why: https://github.com/dchest/tweetnacl-js/blob/master/LICENSE :why: https://github.com/dchest/tweetnacl-js/blob/master/LICENSE
:versions: [] :versions: []
:when: 2017-01-14 20:10:57.812077000 Z :when: 2017-01-14 20:10:57.812077000 Z
- - :approve
- wordwrap
- :who: Mike Greiling
:why: https://github.com/substack/node-wordwrap/blob/0.0.3/LICENSE
:versions: []
:when: 2017-02-08 20:17:13.084968000 Z
- - :approve
- spdx-expression-parse
- :who: Mike Greiling
:why: https://github.com/kemitchell/spdx-expression-parse.js/blob/v1.0.4/LICENSE
:versions: []
:when: 2017-02-08 22:33:01.806977000 Z
- - :approve
- spdx-license-ids
- :who: Mike Greiling
:why: https://github.com/shinnn/spdx-license-ids/blob/v1.2.2/LICENSE
:versions: []
:when: 2017-02-08 22:35:00.225232000 Z
...@@ -48,26 +48,23 @@ var config = { ...@@ -48,26 +48,23 @@ var config = {
devtool: 'inline-source-map', devtool: 'inline-source-map',
module: { module: {
loaders: [ rules: [
{ {
test: /\.(js|es6)$/, test: /\.(js|es6)$/,
exclude: /(node_modules|vendor\/assets)/, exclude: /(node_modules|vendor\/assets)/,
loader: 'babel-loader', loader: 'babel-loader',
query: { options: {
// 'use strict' was broken in sprockets-es6 due to sprockets concatination method. presets: [
// many es5 strict errors which were never caught ended up in our es6 assets as a result. ["es2015", {"modules": false}],
// this hack is necessary until they can be fixed. 'stage-2'
blacklist: ['useStrict'] ]
} }
}, },
{ {
test: /\.(js|es6)$/, test: /\.(js|es6)$/,
exclude: /node_modules/,
loader: 'imports-loader', loader: 'imports-loader',
query: 'this=>window' options: 'this=>window'
},
{
test: /\.json$/,
loader: 'json-loader'
} }
] ]
}, },
...@@ -88,7 +85,7 @@ var config = { ...@@ -88,7 +85,7 @@ var config = {
], ],
resolve: { resolve: {
extensions: ['', '.js', '.es6', '.js.es6'], extensions: ['.js', '.es6', '.js.es6'],
alias: { alias: {
'~': path.join(ROOT_PATH, 'app/assets/javascripts'), '~': path.join(ROOT_PATH, 'app/assets/javascripts'),
'bootstrap/js': 'bootstrap-sass/assets/javascripts/bootstrap', 'bootstrap/js': 'bootstrap-sass/assets/javascripts/bootstrap',
...@@ -104,14 +101,16 @@ if (IS_PRODUCTION) { ...@@ -104,14 +101,16 @@ if (IS_PRODUCTION) {
config.devtool = 'source-map'; config.devtool = 'source-map';
config.plugins.push( config.plugins.push(
new webpack.NoErrorsPlugin(), new webpack.NoErrorsPlugin(),
new webpack.LoaderOptionsPlugin({
minimize: true,
debug: false
}),
new webpack.optimize.UglifyJsPlugin({ new webpack.optimize.UglifyJsPlugin({
compress: { warnings: false } sourceMap: true
}), }),
new webpack.DefinePlugin({ new webpack.DefinePlugin({
'process.env': { NODE_ENV: JSON.stringify('production') } 'process.env': { NODE_ENV: JSON.stringify('production') }
}), })
new webpack.optimize.DedupePlugin(),
new webpack.optimize.OccurrenceOrderPlugin()
); );
} }
......
class AddIndexToLabelsForTitleAndProject < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def change
add_concurrent_index :labels, :title
add_concurrent_index :labels, :project_id
end
end
class AddIndexToCiTriggerRequestsForCommitId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def change
add_concurrent_index :ci_trigger_requests, :commit_id
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: 20170206101030) do ActiveRecord::Schema.define(version: 20170210075922) 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"
...@@ -367,6 +367,8 @@ ActiveRecord::Schema.define(version: 20170206101030) do ...@@ -367,6 +367,8 @@ ActiveRecord::Schema.define(version: 20170206101030) do
t.integer "commit_id" t.integer "commit_id"
end end
add_index "ci_trigger_requests", ["commit_id"], name: "index_ci_trigger_requests_on_commit_id", using: :btree
create_table "ci_triggers", force: :cascade do |t| create_table "ci_triggers", force: :cascade do |t|
t.string "token" t.string "token"
t.integer "project_id" t.integer "project_id"
...@@ -579,6 +581,8 @@ ActiveRecord::Schema.define(version: 20170206101030) do ...@@ -579,6 +581,8 @@ ActiveRecord::Schema.define(version: 20170206101030) do
add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree
add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree
add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
add_index "labels", ["title"], name: "index_labels_on_title", using: :btree
create_table "lfs_objects", force: :cascade do |t| create_table "lfs_objects", force: :cascade do |t|
t.string "oid", null: false t.string "oid", null: false
......
...@@ -11,10 +11,10 @@ API. ...@@ -11,10 +11,10 @@ API.
--- ---
As of GitLab 8.2, GitLab CI is mainly exposed via the `/builds` page of a GitLab CI is exposed via the `/pipelines` and `/builds` pages of a project.
project. Disabling GitLab CI in a project does not delete any previous builds. Disabling GitLab CI in a project does not delete any previous builds.
In fact, the `/builds` page can still be accessed, although it's hidden from In fact, the `/pipelines` and `/builds` pages can still be accessed, although
the left sidebar menu. it's hidden from the left sidebar menu.
GitLab CI is enabled by default on new installations and can be disabled either GitLab CI is enabled by default on new installations and can be disabled either
individually under each project's settings, or site-wide by modifying the individually under each project's settings, or site-wide by modifying the
...@@ -23,12 +23,12 @@ respectively. ...@@ -23,12 +23,12 @@ respectively.
### Per-project user setting ### Per-project user setting
The setting to enable or disable GitLab CI can be found with the name **Builds** The setting to enable or disable GitLab CI can be found with the name **Pipelines**
under the **Features** area of a project's settings along with **Issues**, under the **Sharing & Permissions** area of a project's settings along with
**Merge Requests**, **Wiki** and **Snippets**. Select or deselect the checkbox **Merge Requests**. Choose one of **Disabled**, **Only team members** and
and hit **Save** for the settings to take effect. **Everyone with access** and hit **Save changes** for the settings to take effect.
![Features settings](img/features_settings.png) ![Sharing & Permissions settings](img/permissions_settings.png)
--- ---
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
...@@ -13,6 +13,28 @@ executed. ...@@ -13,6 +13,28 @@ executed.
![Pipelines example](img/pipelines.png) ![Pipelines example](img/pipelines.png)
## Types of Pipelines
There are three types of pipelines that often use the single shorthand of "pipeline". People often talk about them as if each one is "the" pipeline, but really, they're just pieces of a single, comprehensive pipeline.
![Types of Pipelines](img/types-of-pipelines.svg)
1. **CI Pipeline**: Build and test stages defined in `.gitlab-ci.yml`
2. **Deploy Pipeline**: Deploy stage(s) defined in `.gitlab-ci.yml` The flow of deploying code to servers through various stages: e.g. development to staging to production
3. **Project Pipeline**: Cross-project CI dependencies [triggered via API]((triggers)), particularly for micro-services, but also for complicated build dependencies: e.g. api -> front-end, ce/ee -> omnibus.
## Development Workflows
Pipelines accommodate several development workflows:
1. **Branch Flow** (e.g. different branch for dev, qa, staging, production)
2. **Trunk-based Flow** (e.g. feature branches and single master branch, possibly with tags for releases)
3. **Fork-based Flow** (e.g. merge requests come from forks)
Example continuous delivery flow:
![CD Flow](img/pipelines-goal.svg)
## Builds ## Builds
Builds are individual runs of [jobs]. Not to be confused with a `build` job or Builds are individual runs of [jobs]. Not to be confused with a `build` job or
......
...@@ -250,23 +250,17 @@ information. ...@@ -250,23 +250,17 @@ information.
### Running frontend tests ### Running frontend tests
`rake teaspoon` runs the frontend-only (JavaScript) tests. `rake karma` runs the frontend-only (JavaScript) tests.
It consists of two subtasks: It consists of two subtasks:
- `rake teaspoon:fixtures` (re-)generates fixtures - `rake karma:fixtures` (re-)generates fixtures
- `rake teaspoon:tests` actually executes the tests - `rake karma:tests` actually executes the tests
As long as the fixtures don't change, `rake teaspoon:tests` is sufficient As long as the fixtures don't change, `rake karma:tests` is sufficient
(and saves you some time). (and saves you some time).
If you need to debug your tests and/or application code while they're
running, navigate to [localhost:3000/teaspoon](http://localhost:3000/teaspoon)
in your browser, open DevTools, and run tests for individual files by clicking
on them. This is also much faster than setting up and running tests from the
command line.
Please note: Not all of the frontend fixtures are generated. Some are still static Please note: Not all of the frontend fixtures are generated. Some are still static
files. These will not be touched by `rake teaspoon:fixtures`. files. These will not be touched by `rake karma:fixtures`.
## Design Patterns ## Design Patterns
...@@ -370,7 +364,7 @@ For our currently-supported browsers, see our [requirements][requirements]. ...@@ -370,7 +364,7 @@ For our currently-supported browsers, see our [requirements][requirements].
### Spec errors due to use of ES6 features in `.js` files ### Spec errors due to use of ES6 features in `.js` files
If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being
thrown in Teaspoon, Spinach, or Rspec tests but can't reproduce them manually, thrown in Karma, Spinach, or Rspec tests but can't reproduce them manually,
you may have included `ES6`-style JavaScript in files that don't have the you may have included `ES6`-style JavaScript in files that don't have the
`.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file `.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file
you're working in (`git mv <file.js> <file.js.es6>`). you're working in (`git mv <file.js> <file.js.es6>`).
......
...@@ -17,14 +17,14 @@ Note: `db:setup` calls `db:seed` but this does nothing. ...@@ -17,14 +17,14 @@ Note: `db:setup` calls `db:seed` but this does nothing.
In order to run the test you can use the following commands: In order to run the test you can use the following commands:
- `rake spinach` to run the spinach suite - `rake spinach` to run the spinach suite
- `rake spec` to run the rspec suite - `rake spec` to run the rspec suite
- `rake teaspoon` to run the teaspoon test suite - `rake karma` to run the karma test suite
- `rake gitlab:test` to run all the tests - `rake gitlab:test` to run all the tests
Note: Both `rake spinach` and `rake spec` takes significant time to pass. Note: Both `rake spinach` and `rake spec` takes significant time to pass.
Instead of running full test suite locally you can save a lot of time by running Instead of running full test suite locally you can save a lot of time by running
a single test or directory related to your changes. After you submit merge request a single test or directory related to your changes. After you submit merge request
CI will run full test suite for you. Green CI status in the merge request means CI will run full test suite for you. Green CI status in the merge request means
full test suite is passed. full test suite is passed.
Note: You can't run `rspec .` since this will try to run all the `_spec.rb` Note: You can't run `rspec .` since this will try to run all the `_spec.rb`
files it can find, also the ones in `/tmp` files it can find, also the ones in `/tmp`
......
...@@ -31,9 +31,8 @@ GitLab uses [factory_girl] as a test fixture replacement. ...@@ -31,9 +31,8 @@ GitLab uses [factory_girl] as a test fixture replacement.
## JavaScript ## JavaScript
GitLab uses [Teaspoon] to run its [Jasmine] JavaScript specs. They can be run on GitLab uses [Karma] to run its [Jasmine] JavaScript specs. They can be run on
the command line via `bundle exec teaspoon`, or via a web browser at the command line via `bundle exec karma`.
`http://localhost:3000/teaspoon` when the Rails server is running.
- JavaScript tests live in `spec/javascripts/`, matching the folder structure of - JavaScript tests live in `spec/javascripts/`, matching the folder structure of
`app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.es6` has a corresponding `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.es6` has a corresponding
...@@ -51,7 +50,7 @@ the command line via `bundle exec teaspoon`, or via a web browser at ...@@ -51,7 +50,7 @@ the command line via `bundle exec teaspoon`, or via a web browser at
[`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification), [`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification),
which will have to be stubbed. which will have to be stubbed.
[Teaspoon]: https://github.com/modeset/teaspoon [Karma]: https://github.com/karma-runner/karma
[Jasmine]: https://github.com/jasmine/jasmine [Jasmine]: https://github.com/jasmine/jasmine
## RSpec ## RSpec
......
doc/user/project/img/issue_board.png

88.5 KB | W: | H:

doc/user/project/img/issue_board.png

74.7 KB | W: | H:

doc/user/project/img/issue_board.png
doc/user/project/img/issue_board.png
doc/user/project/img/issue_board.png
doc/user/project/img/issue_board.png
  • 2-up
  • Swipe
  • Onion skin
# Issue board # Issue board
> [Introduced][ce-5554] in GitLab 8.11. >**Notes:**
- [Introduced][ce-5554] in GitLab 8.11.
- The Backlog column was replaced by the **Add issues** button in GitLab 8.17.
The GitLab Issue Board is a software project management tool used to plan, The GitLab Issue Board is a software project management tool used to plan,
organize, and visualize a workflow for a feature or product release. organize, and visualize a workflow for a feature or product release.
...@@ -28,13 +30,11 @@ Below is a table of the definitions used for GitLab's Issue Board. ...@@ -28,13 +30,11 @@ Below is a table of the definitions used for GitLab's Issue Board.
| **List** | Each label that exists in the issue tracker can have its own dedicated list. Every list is named after the label it is based on and is represented by a column which contains all the issues associated with that label. You can think of a list like the results you get when you filter the issues by a label in your issue tracker. | | **List** | Each label that exists in the issue tracker can have its own dedicated list. Every list is named after the label it is based on and is represented by a column which contains all the issues associated with that label. You can think of a list like the results you get when you filter the issues by a label in your issue tracker. |
| **Card** | Every card represents an issue and it is shown under the list for which it has a label. The information you can see on a card consists of the issue number, the issue title, the assignee and the labels associated with it. You can drag cards around from one list to another. Issues inside lists are [ordered by priority](labels.md#prioritize-labels). | | **Card** | Every card represents an issue and it is shown under the list for which it has a label. The information you can see on a card consists of the issue number, the issue title, the assignee and the labels associated with it. You can drag cards around from one list to another. Issues inside lists are [ordered by priority](labels.md#prioritize-labels). |
There are three types of lists, the ones you create based on your labels, and There are two types of lists, the ones you create based on your labels, and
two default: one default:
- **Backlog** (default): shows all issues that do not fall in one of the other lists. Always appears on the very left.
- **Done** (default): shows all closed issues. Always appears on the very right.
Label list: a list based on a label. It shows all issues with that label.
- Label list: a list based on a label. It shows all opened issues with that label. - Label list: a list based on a label. It shows all opened issues with that label.
- **Done** (default): shows all closed issues. Always appears on the very right.
![GitLab Issue Board](img/issue_board.png) ![GitLab Issue Board](img/issue_board.png)
...@@ -55,10 +55,10 @@ In short, here's a list of actions you can take in an Issue Board: ...@@ -55,10 +55,10 @@ In short, here's a list of actions you can take in an Issue Board:
If you are not able to perform one or more of the things above, make sure you If you are not able to perform one or more of the things above, make sure you
have the right [permissions](#permissions). have the right [permissions](#permissions).
## First time using the Issue Board ## First time using the issue board
The first time you navigate to your Issue Board, you will be presented with the The first time you navigate to your Issue Board, you will be presented with
two default lists (**Backlog** and **Done**) and a welcoming message that gives a default list (**Done**) and a welcoming message that gives
you two options. You can either create a predefined set of labels and create you two options. You can either create a predefined set of labels and create
their corresponding lists to the Issue Board or opt-out and use your own lists. their corresponding lists to the Issue Board or opt-out and use your own lists.
...@@ -93,23 +93,26 @@ in the list's heading. A confirmation dialog will appear for you to confirm. ...@@ -93,23 +93,26 @@ in the list's heading. A confirmation dialog will appear for you to confirm.
Deleting a list doesn't have any effect in issues and labels, it's just the Deleting a list doesn't have any effect in issues and labels, it's just the
list view that is removed. You can always add it back later if you need. list view that is removed. You can always add it back later if you need.
## Searching issues in the Backlog list ## Adding issues to a list
You can add issues to a list by clicking the **Add issues** button that is
present in the upper right corner of the issue board. This will open up a modal
window where you can see all the issues that do not belong to any list.
Select one or more issues by clicking on the cards and then click **Add issues**
to add them to the selected list. You can limit the issues you want to add to
the list by filtering by author, assignee, milestone and label.
The very first time you start using the Issue Board, it is very likely your ![Bulk adding issues to lists](img/issue_boards_add_issues_modal.png)
issue tracker is already populated with labels and issues. In that case,
**Backlog** will have all the issues that don't belong to another list, and
**Done** will have all the closed ones.
For performance and visibility reasons, each list shows the first 20 issues ## Removing an issue from a list
by default. If you have more than 20, you have to start scrolling down for the
next 20 issues to appear. This can be cumbersome if your issue tracker hosts
hundreds of issues, and for that reason it is easier to search for issues to
move from **Backlog** to another list.
Start typing in the search bar under the **Backlog** list and the relevant Removing an issue from a list can be done by clicking on the issue card and then
issues will appear. clicking the **Remove from board** button in the sidebar. Under the hood, the
respective label is removed, and as such it's also removed from the list and the
board itself.
![Issue Board search Backlog](img/issue_board_search_backlog.png) ![Remove issue from list](img/issue_boards_remove_issue.png)
## Filtering issues ## Filtering issues
...@@ -142,8 +145,8 @@ A typical workflow of using the Issue Board would be: ...@@ -142,8 +145,8 @@ A typical workflow of using the Issue Board would be:
and gets automatically closed. and gets automatically closed.
For instance you can create a list based on the label of 'Frontend' and one for For instance you can create a list based on the label of 'Frontend' and one for
'Backend'. A designer can start working on an issue by dragging it from 'Backend'. A designer can start working on an issue by adding it to the
**Backlog** to 'Frontend'. That way, everyone knows that this issue is now being 'Frontend' list. That way, everyone knows that this issue is now being
worked on by the designers. Then, once they're done, all they have to do is worked on by the designers. Then, once they're done, all they have to do is
drag it over to the next list, 'Backend', where a backend developer can drag it over to the next list, 'Backend', where a backend developer can
eventually pick it up. Once they’re done, they move it to **Done**, to close the eventually pick it up. Once they’re done, they move it to **Done**, to close the
......
# Merge Requests This document was moved to [merge_requests/index.md](merge_requests/index.md).
Merge requests allow you to exchange changes you made to source code and
collaborate with other people on the same project.
## Authorization for merge requests
There are two main ways to have a merge request flow with GitLab:
1. Working with [protected branches][] in a single repository
1. Working with forks of an authoritative project
[Learn more about the authorization for merge requests.](merge_requests/authorization_for_merge_requests.md)
## Cherry-pick changes
Cherry-pick any commit in the UI by simply clicking the **Cherry-pick** button
in a merged merge requests or a commit.
[Learn more about cherry-picking changes.](merge_requests/cherry_pick_changes.md)
## Merge when pipeline succeeds
When reviewing a merge request that looks ready to merge but still has one or
more CI builds running, you can set it to be merged automatically when CI
pipeline succeeds. This way, you don't have to wait for the pipeline to finish
and remember to merge the request manually.
[Learn more about merging when pipeline succeeds.](merge_requests/merge_when_pipeline_succeeds.md)
## Resolve discussion comments in merge requests reviews
Keep track of the progress during a code review with resolving comments.
Resolving comments prevents you from forgetting to address feedback and lets
you hide discussions that are no longer relevant.
[Read more about resolving discussion comments in merge requests reviews.](merge_requests/merge_request_discussion_resolution.md)
## Resolve conflicts
When a merge request has conflicts, GitLab may provide the option to resolve
those conflicts in the GitLab UI.
[Learn more about resolving merge conflicts in the UI.](merge_requests/resolve_conflicts.md)
## Revert changes
GitLab implements Git's powerful feature to revert any commit with introducing
a **Revert** button in merge requests and commit details.
[Learn more about reverting changes in the UI](merge_requests/revert_changes.md)
## Merge requests versions
Every time you push to a branch that is tied to a merge request, a new version
of merge request diff is created. When you visit a merge request that contains
more than one pushes, you can select and compare the versions of those merge
request diffs.
[Read more about the merge requests versions.](merge_requests/versions.md)
## Work In Progress merge requests
To prevent merge requests from accidentally being accepted before they're
completely ready, GitLab blocks the "Accept" button for merge requests that
have been marked as a **Work In Progress**.
[Learn more about settings a merge request as "Work In Progress".](merge_requests/work_in_progress_merge_requests.md)
## Ignore whitespace changes in Merge Request diff view
If you click the **Hide whitespace changes** button, you can see the diff
without whitespace changes (if there are any). This is also working when on a
specific commit page.
![MR diff](merge_requests/img/merge_request_diff.png)
>**Tip:**
You can append `?w=1` while on the diffs page of a merge request to ignore any
whitespace changes.
## Tips
Here are some tips that will help you be more efficient with merge requests in
the command line.
> **Note:**
This section might move in its own document in the future.
### Checkout merge requests locally
A merge request contains all the history from a repository, plus the additional
commits added to the branch associated with the merge request. Here's a few
tricks to checkout a merge request locally.
Please note that you can checkout a merge request locally even if the source
project is a fork (even a private fork) of the target project.
#### Checkout locally by adding a git alias
Add the following alias to your `~/.gitconfig`:
```
[alias]
mr = !sh -c 'git fetch $1 merge-requests/$2/head:mr-$1-$2 && git checkout mr-$1-$2' -
```
Now you can check out a particular merge request from any repository and any
remote. For example, to check out the merge request with ID 5 as shown in GitLab
from the `upstream` remote, do:
```
git mr upstream 5
```
This will fetch the merge request into a local `mr-upstream-5` branch and check
it out.
#### Checkout locally by modifying `.git/config` for a given repository
Locate the section for your GitLab remote in the `.git/config` file. It looks
like this:
```
[remote "origin"]
url = https://gitlab.com/gitlab-org/gitlab-ce.git
fetch = +refs/heads/*:refs/remotes/origin/*
```
You can open the file with:
```
git config -e
```
Now add the following line to the above section:
```
fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
```
In the end, it should look like this:
```
[remote "origin"]
url = https://gitlab.com/gitlab-org/gitlab-ce.git
fetch = +refs/heads/*:refs/remotes/origin/*
fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
```
Now you can fetch all the merge requests:
```
git fetch origin
...
From https://gitlab.com/gitlab-org/gitlab-ce.git
* [new ref] refs/merge-requests/1/head -> origin/merge-requests/1
* [new ref] refs/merge-requests/2/head -> origin/merge-requests/2
...
```
And to check out a particular merge request:
```
git checkout origin/merge-requests/1
```
[protected branches]: protected_branches.md
# Merge requests
Merge requests allow you to exchange changes you made to source code and
collaborate with other people on the same project.
## Authorization for merge requests
There are two main ways to have a merge request flow with GitLab:
1. Working with [protected branches][] in a single repository
1. Working with forks of an authoritative project
[Learn more about the authorization for merge requests.](authorization_for_merge_requests.md)
## Cherry-pick changes
Cherry-pick any commit in the UI by simply clicking the **Cherry-pick** button
in a merged merge requests or a commit.
[Learn more about cherry-picking changes.](cherry_pick_changes.md)
## Merge when pipeline succeeds
When reviewing a merge request that looks ready to merge but still has one or
more CI builds running, you can set it to be merged automatically when CI
pipeline succeeds. This way, you don't have to wait for the pipeline to finish
and remember to merge the request manually.
[Learn more about merging when pipeline succeeds.](merge_when_pipeline_succeeds.md)
## Resolve discussion comments in merge requests reviews
Keep track of the progress during a code review with resolving comments.
Resolving comments prevents you from forgetting to address feedback and lets
you hide discussions that are no longer relevant.
[Read more about resolving discussion comments in merge requests reviews.](merge_request_discussion_resolution.md)
## Resolve conflicts
When a merge request has conflicts, GitLab may provide the option to resolve
those conflicts in the GitLab UI.
[Learn more about resolving merge conflicts in the UI.](resolve_conflicts.md)
## Revert changes
GitLab implements Git's powerful feature to revert any commit with introducing
a **Revert** button in merge requests and commit details.
[Learn more about reverting changes in the UI](revert_changes.md)
## Merge requests versions
Every time you push to a branch that is tied to a merge request, a new version
of merge request diff is created. When you visit a merge request that contains
more than one pushes, you can select and compare the versions of those merge
request diffs.
[Read more about the merge requests versions.](versions.md)
## Work In Progress merge requests
To prevent merge requests from accidentally being accepted before they're
completely ready, GitLab blocks the "Accept" button for merge requests that
have been marked as a **Work In Progress**.
[Learn more about settings a merge request as "Work In Progress".](work_in_progress_merge_requests.md)
## Ignore whitespace changes in Merge Request diff view
If you click the **Hide whitespace changes** button, you can see the diff
without whitespace changes (if there are any). This is also working when on a
specific commit page.
![MR diff](img/merge_request_diff.png)
>**Tip:**
You can append `?w=1` while on the diffs page of a merge request to ignore any
whitespace changes.
## Tips
Here are some tips that will help you be more efficient with merge requests in
the command line.
> **Note:**
This section might move in its own document in the future.
### Checkout merge requests locally
A merge request contains all the history from a repository, plus the additional
commits added to the branch associated with the merge request. Here's a few
tricks to checkout a merge request locally.
Please note that you can checkout a merge request locally even if the source
project is a fork (even a private fork) of the target project.
#### Checkout locally by adding a git alias
Add the following alias to your `~/.gitconfig`:
```
[alias]
mr = !sh -c 'git fetch $1 merge-requests/$2/head:mr-$1-$2 && git checkout mr-$1-$2' -
```
Now you can check out a particular merge request from any repository and any
remote. For example, to check out the merge request with ID 5 as shown in GitLab
from the `upstream` remote, do:
```
git mr upstream 5
```
This will fetch the merge request into a local `mr-upstream-5` branch and check
it out.
#### Checkout locally by modifying `.git/config` for a given repository
Locate the section for your GitLab remote in the `.git/config` file. It looks
like this:
```
[remote "origin"]
url = https://gitlab.com/gitlab-org/gitlab-ce.git
fetch = +refs/heads/*:refs/remotes/origin/*
```
You can open the file with:
```
git config -e
```
Now add the following line to the above section:
```
fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
```
In the end, it should look like this:
```
[remote "origin"]
url = https://gitlab.com/gitlab-org/gitlab-ce.git
fetch = +refs/heads/*:refs/remotes/origin/*
fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
```
Now you can fetch all the merge requests:
```
git fetch origin
...
From https://gitlab.com/gitlab-org/gitlab-ce.git
* [new ref] refs/merge-requests/1/head -> origin/merge-requests/1
* [new ref] refs/merge-requests/2/head -> origin/merge-requests/2
...
```
And to check out a particular merge request:
```
git checkout origin/merge-requests/1
```
[protected branches]: protected_branches.md
# Merge requests versions # Merge requests versions
> Will be [introduced][ce-5467] in GitLab 8.12. >**Notes:**
- [Introduced][ce-5467] in GitLab 8.12.
- Comments are disabled while viewing outdated merge versions or comparing to
versions other than base.
- Merge request versions are based on push not on commit. So, if you pushed 5
commits in a single push, it will be a single option in the dropdown. If you
pushed 5 times, that will count for 5 options.
Every time you push to a branch that is tied to a merge request, a new version Every time you push to a branch that is tied to a merge request, a new version
of merge request diff is created. When you visit a merge request that contains of merge request diff is created. When you visit a merge request that contains
...@@ -30,13 +36,4 @@ changes appears as a system note. ...@@ -30,13 +36,4 @@ changes appears as a system note.
![Merge request versions system note](img/versions_system_note.png) ![Merge request versions system note](img/versions_system_note.png)
---
>**Notes:**
- Comments are disabled while viewing outdated merge versions or comparing to
versions other than base.
- Merge request versions are based on push not on commit. So, if you pushed 5
commits in a single push, it will be a single option in the dropdown. If you
pushed 5 times, that will count for 5 options.
[ce-5467]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5467 [ce-5467]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5467
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
- [Web Editor](../user/project/repository/web_editor.md) - [Web Editor](../user/project/repository/web_editor.md)
- [Releases](releases.md) - [Releases](releases.md)
- [Milestones](milestones.md) - [Milestones](milestones.md)
- [Merge Requests](../user/project/merge_requests.md) - [Merge Requests](../user/project/merge_requests/index.md)
- [Authorization for merge requests](../user/project/merge_requests/authorization_for_merge_requests.md) - [Authorization for merge requests](../user/project/merge_requests/authorization_for_merge_requests.md)
- [Cherry-pick changes](../user/project/merge_requests/cherry_pick_changes.md) - [Cherry-pick changes](../user/project/merge_requests/cherry_pick_changes.md)
- [Merge when pipeline succeeds](../user/project/merge_requests/merge_when_pipeline_succeeds.md) - [Merge when pipeline succeeds](../user/project/merge_requests/merge_when_pipeline_succeeds.md)
......
...@@ -6,7 +6,7 @@ module Gitlab ...@@ -6,7 +6,7 @@ module Gitlab
MAX_INT_VALUE = 2147483647 MAX_INT_VALUE = 2147483647
def self.adapter_name def self.adapter_name
connection.adapter_name ActiveRecord::Base.configurations[Rails.env]['adapter']
end end
def self.mysql? def self.mysql?
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment