Commit 4b80cde2 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into multi-file-editor-vuex

parents 47c906eb 6919f636
...@@ -21,10 +21,10 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._ ...@@ -21,10 +21,10 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Workflow labels](#workflow-labels) - [Workflow labels](#workflow-labels)
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc) - [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc) - [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-ci-discussion-edge-platform-etc) - [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc)
- [Priority labels (~Deliverable and ~Stretch)](#priority-labels-deliverable-and-stretch) - [Priority labels (~Deliverable and ~Stretch)](#priority-labels-deliverable-and-stretch)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests) - [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design--ui-elements) - [Implement design & UI elements](#implement-design-ui-elements)
- [Issue tracker](#issue-tracker) - [Issue tracker](#issue-tracker)
- [Issue triaging](#issue-triaging) - [Issue triaging](#issue-triaging)
- [Feature proposals](#feature-proposals) - [Feature proposals](#feature-proposals)
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */ /* eslint-disable no-param-reassign, prefer-template, no-var, no-void, consistent-return */
import AccessorUtilities from './lib/utils/accessor'; import AccessorUtilities from './lib/utils/accessor';
window.Autosave = (function() { export default class Autosave {
function Autosave(field, key, resource) { constructor(field, key, resource) {
this.field = field; this.field = field;
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.resource = resource; this.resource = resource;
...@@ -12,14 +13,10 @@ window.Autosave = (function() { ...@@ -12,14 +13,10 @@ window.Autosave = (function() {
this.key = 'autosave/' + key; this.key = 'autosave/' + key;
this.field.data('autosave', this); this.field.data('autosave', this);
this.restore(); this.restore();
this.field.on('input', (function(_this) { this.field.on('input', () => this.save());
return function() {
return _this.save();
};
})(this));
} }
Autosave.prototype.restore = function() { restore() {
var text; var text;
if (!this.isLocalStorageAvailable) return; if (!this.isLocalStorageAvailable) return;
...@@ -40,9 +37,9 @@ window.Autosave = (function() { ...@@ -40,9 +37,9 @@ window.Autosave = (function() {
field.dispatchEvent(event); field.dispatchEvent(event);
} }
} }
}; }
Autosave.prototype.save = function() { save() {
var text; var text;
text = this.field.val(); text = this.field.val();
...@@ -51,15 +48,11 @@ window.Autosave = (function() { ...@@ -51,15 +48,11 @@ window.Autosave = (function() {
} }
return this.reset(); return this.reset();
}; }
Autosave.prototype.reset = function() { reset() {
if (!this.isLocalStorageAvailable) return; if (!this.isLocalStorageAvailable) return;
return window.localStorage.removeItem(this.key); return window.localStorage.removeItem(this.key);
}; }
}
return Autosave;
})();
export default window.Autosave;
...@@ -11,7 +11,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { ...@@ -11,7 +11,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
// Issue boards is slightly different, we handle all the requests async // Issue boards is slightly different, we handle all the requests async
// instead or reloading the page, we just re-fire the list ajax requests // instead or reloading the page, we just re-fire the list ajax requests
this.isHandledAsync = true; this.isHandledAsync = true;
this.cantEdit = cantEdit; this.cantEdit = cantEdit.filter(i => typeof i === 'string');
this.cantEditWithValue = cantEdit.filter(i => typeof i === 'object');
} }
updateObject(path) { updateObject(path) {
...@@ -42,7 +43,9 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { ...@@ -42,7 +43,9 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
this.filteredSearchInput.dispatchEvent(new Event('input')); this.filteredSearchInput.dispatchEvent(new Event('input'));
} }
canEdit(tokenName) { canEdit(tokenName, tokenValue) {
return this.cantEdit.indexOf(tokenName) === -1; if (this.cantEdit.includes(tokenName)) return false;
return this.cantEditWithValue.findIndex(token => token.name === tokenName &&
token.value === tokenValue) === -1;
} }
} }
...@@ -14,16 +14,18 @@ gl.issueBoards.BoardsStore = { ...@@ -14,16 +14,18 @@ gl.issueBoards.BoardsStore = {
}, },
state: {}, state: {},
detail: { detail: {
issue: {} issue: {},
}, },
moving: { moving: {
issue: {}, issue: {},
list: {} list: {},
}, },
create () { create () {
this.state.lists = []; this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&'); this.filter.path = getUrlParamsArray().join('&');
this.detail = { issue: {} }; this.detail = {
issue: {},
};
}, },
addList (listObj, defaultAvatar) { addList (listObj, defaultAvatar) {
const list = new List(listObj, defaultAvatar); const list = new List(listObj, defaultAvatar);
......
...@@ -13,7 +13,7 @@ import GroupLabelSubscription from './group_label_subscription'; ...@@ -13,7 +13,7 @@ import GroupLabelSubscription from './group_label_subscription';
/* global LineHighlighter */ /* global LineHighlighter */
import BuildArtifacts from './build_artifacts'; import BuildArtifacts from './build_artifacts';
import CILintEditor from './ci_lint_editor'; import CILintEditor from './ci_lint_editor';
/* global GroupsSelect */ import groupsSelect from './groups_select';
/* global Search */ /* global Search */
/* global Admin */ /* global Admin */
/* global NamespaceSelects */ /* global NamespaceSelects */
...@@ -414,7 +414,7 @@ import Diff from './diff'; ...@@ -414,7 +414,7 @@ import Diff from './diff';
break; break;
case 'projects:project_members:index': case 'projects:project_members:index':
memberExpirationDate('.js-access-expiration-date-groups'); memberExpirationDate('.js-access-expiration-date-groups');
new GroupsSelect(); groupsSelect();
memberExpirationDate(); memberExpirationDate();
new Members(); new Members();
new UsersSelect(); new UsersSelect();
......
...@@ -147,6 +147,16 @@ class DropdownUtils { ...@@ -147,6 +147,16 @@ class DropdownUtils {
return dataValue !== null; return dataValue !== null;
} }
static getVisualTokenValues(visualToken) {
const tokenName = visualToken && visualToken.querySelector('.name').textContent.trim();
let tokenValue = visualToken && visualToken.querySelector('.value') && visualToken.querySelector('.value').textContent.trim();
if (tokenName === 'label' && tokenValue) {
// remove leading symbol and wrapping quotes
tokenValue = tokenValue.replace(/^~("|')?(.*)/, '$2').replace(/("|')$/, '');
}
return { tokenName, tokenValue };
}
// Determines the full search query (visual tokens + input) // Determines the full search query (visual tokens + input)
static getSearchQuery(untilInput = false) { static getSearchQuery(untilInput = false) {
const container = FilteredSearchContainer.container; const container = FilteredSearchContainer.container;
......
...@@ -185,8 +185,8 @@ class FilteredSearchManager { ...@@ -185,8 +185,8 @@ class FilteredSearchManager {
if (e.keyCode === 8 || e.keyCode === 46) { if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const sanitizedTokenName = lastVisualToken && lastVisualToken.querySelector('.name').textContent.trim(); const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken);
const canEdit = sanitizedTokenName && this.canEdit && this.canEdit(sanitizedTokenName); const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue);
if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) { if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial(); this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial(); gl.FilteredSearchVisualTokens.removeLastTokenPartial();
...@@ -336,8 +336,8 @@ class FilteredSearchManager { ...@@ -336,8 +336,8 @@ class FilteredSearchManager {
let canClearToken = t.classList.contains('js-visual-token'); let canClearToken = t.classList.contains('js-visual-token');
if (canClearToken) { if (canClearToken) {
const tokenKey = t.querySelector('.name').textContent.trim(); const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(t);
canClearToken = this.canEdit && this.canEdit(tokenKey); canClearToken = this.canEdit && this.canEdit(tokenName, tokenValue);
} }
if (canClearToken) { if (canClearToken) {
...@@ -469,7 +469,7 @@ class FilteredSearchManager { ...@@ -469,7 +469,7 @@ class FilteredSearchManager {
} }
hasFilteredSearch = true; hasFilteredSearch = true;
const canEdit = this.canEdit && this.canEdit(sanitizedKey); const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue);
gl.FilteredSearchVisualTokens.addFilterVisualToken( gl.FilteredSearchVisualTokens.addFilterVisualToken(
sanitizedKey, sanitizedKey,
`${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`,
......
...@@ -38,21 +38,14 @@ class FilteredSearchVisualTokens { ...@@ -38,21 +38,14 @@ class FilteredSearchVisualTokens {
} }
static createVisualTokenElementHTML(canEdit = true) { static createVisualTokenElementHTML(canEdit = true) {
let removeTokenMarkup = '';
if (canEdit) {
removeTokenMarkup = `
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
`;
}
return ` return `
<div class="selectable" role="button"> <div class="${canEdit ? 'selectable' : 'hidden'}" role="button">
<div class="name"></div> <div class="name"></div>
<div class="value-container"> <div class="value-container">
<div class="value"></div> <div class="value"></div>
${removeTokenMarkup} <div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
</div> </div>
</div> </div>
`; `;
......
/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var,
camelcase, one-var-declaration-per-line, quotes, object-shorthand,
prefer-arrow-callback, comma-dangle, consistent-return, yoda,
prefer-rest-params, prefer-spread, no-unused-vars, prefer-template,
promise/catch-or-return */
import Api from './api'; import Api from './api';
import { normalizeCRLFHeaders } from './lib/utils/common_utils'; import { normalizeCRLFHeaders } from './lib/utils/common_utils';
var slice = [].slice; export default function groupsSelect() {
// Needs to be accessible in rspec
window.GROUP_SELECT_PER_PAGE = 20;
$('.ajax-groups-select').each(function setAjaxGroupsSelect2() {
const $select = $(this);
const allAvailable = $select.data('all-available');
const skipGroups = $select.data('skip-groups') || [];
$select.select2({
placeholder: 'Search for a group',
multiple: $select.hasClass('multiselect'),
minimumInputLength: 0,
ajax: {
url: Api.buildUrl(Api.groupsPath),
dataType: 'json',
quietMillis: 250,
transport(params) {
return $.ajax(params)
.then((data, status, xhr) => {
const results = data || [];
window.GroupsSelect = (function() { const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders());
function GroupsSelect() { const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
$('.ajax-groups-select').each((function(_this) { const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
const self = _this; const more = currentPage < totalPages;
return function(i, select) {
var all_available, skip_groups;
const $select = $(select);
all_available = $select.data('all-available');
skip_groups = $select.data('skip-groups') || [];
$select.select2({
placeholder: "Search for a group",
multiple: $select.hasClass('multiselect'),
minimumInputLength: 0,
ajax: {
url: Api.buildUrl(Api.groupsPath),
dataType: 'json',
quietMillis: 250,
transport: function (params) {
$.ajax(params).then((data, status, xhr) => {
const results = data || [];
const headers = normalizeCRLFHeaders(xhr.getAllResponseHeaders());
const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
const more = currentPage < totalPages;
return {
results,
pagination: {
more,
},
};
}).then(params.success).fail(params.error);
},
data: function (search, page) {
return {
search,
page,
per_page: GroupsSelect.PER_PAGE,
all_available,
};
},
results: function (data, page) {
if (data.length) return { results: [] };
const groups = data.length ? data : data.results || [];
const more = data.pagination ? data.pagination.more : false;
const results = groups.filter(group => skip_groups.indexOf(group.id) === -1);
return { return {
results, results,
page, pagination: {
more, more,
},
}; };
}, })
}, .then(params.success)
initSelection: function(element, callback) { .fail(params.error);
var id; },
id = $(element).val(); data(search, page) {
if (id !== "") { return {
return Api.group(id, callback); search,
} page,
}, per_page: window.GROUP_SELECT_PER_PAGE,
formatResult: function() { all_available: allAvailable,
var args; };
args = 1 <= arguments.length ? slice.call(arguments, 0) : []; },
return self.formatResult.apply(self, args); results(data, page) {
}, if (data.length) return { results: [] };
formatSelection: function() {
var args;
args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
return self.formatSelection.apply(self, args);
},
dropdownCssClass: "ajax-groups-dropdown select2-infinite",
// we do not want to escape markup since we are displaying html in results
escapeMarkup: function(m) {
return m;
}
});
self.dropdown = document.querySelector('.select2-infinite .select2-results');
$select.on('select2-loaded', self.forceOverflow.bind(self));
};
})(this));
}
GroupsSelect.prototype.formatResult = function(group) {
var avatar;
if (group.avatar_url) {
avatar = group.avatar_url;
} else {
avatar = gon.default_avatar_url;
}
return "<div class='group-result'> <div class='group-name'>" + group.full_name + "</div> <div class='group-path'>" + group.full_path + "</div> </div>";
};
GroupsSelect.prototype.formatSelection = function(group) {
return group.full_name;
};
GroupsSelect.prototype.forceOverflow = function (e) { const groups = data.length ? data : data.results || [];
this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight)}px`; const more = data.pagination ? data.pagination.more : false;
}; const results = groups.filter(group => skipGroups.indexOf(group.id) === -1);
GroupsSelect.PER_PAGE = 20; return {
results,
page,
more,
};
},
},
// eslint-disable-next-line consistent-return
initSelection(element, callback) {
const id = $(element).val();
if (id !== '') {
return Api.group(id, callback);
}
},
formatResult(object) {
return `<div class='group-result'> <div class='group-name'>${object.full_name}</div> <div class='group-path'>${object.full_path}</div> </div>`;
},
formatSelection(object) {
return object.full_name;
},
dropdownCssClass: 'ajax-groups-dropdown select2-infinite',
// we do not want to escape markup since we are displaying html in results
escapeMarkup(m) {
return m;
},
});
return GroupsSelect; $select.on('select2-loaded', () => {
})(); const dropdown = document.querySelector('.select2-infinite .select2-results');
dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`;
});
});
}
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */
/* global GitLab */ /* global GitLab */
/* global Autosave */
import Pikaday from 'pikaday'; import Pikaday from 'pikaday';
import Autosave from './autosave';
import UsersSelect from './users_select'; import UsersSelect from './users_select';
import GfmAutoComplete from './gfm_auto_complete'; import GfmAutoComplete from './gfm_auto_complete';
import ZenMode from './zen_mode'; import ZenMode from './zen_mode';
......
...@@ -8,7 +8,7 @@ import CreateLabelDropdown from './create_label'; ...@@ -8,7 +8,7 @@ import CreateLabelDropdown from './create_label';
(function() { (function() {
this.LabelsSelect = (function() { this.LabelsSelect = (function() {
function LabelsSelect(els) { function LabelsSelect(els, options = {}) {
var _this, $els; var _this, $els;
_this = this; _this = this;
...@@ -58,6 +58,7 @@ import CreateLabelDropdown from './create_label'; ...@@ -58,6 +58,7 @@ import CreateLabelDropdown from './create_label';
labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>'); labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>');
labelNoneHTMLTemplate = '<span class="no-value">None</span>'; labelNoneHTMLTemplate = '<span class="no-value">None</span>';
} }
const handleClick = options.handleClick;
$sidebarLabelTooltip.tooltip(); $sidebarLabelTooltip.tooltip();
...@@ -316,9 +317,9 @@ import CreateLabelDropdown from './create_label'; ...@@ -316,9 +317,9 @@ import CreateLabelDropdown from './create_label';
}, },
multiSelect: $dropdown.hasClass('js-multiselect'), multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'), vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(options) { clicked: function(clickEvent) {
const { $el, e, isMarking } = options; const { $el, e, isMarking } = clickEvent;
const label = options.selectedObj; const label = clickEvent.selectedObj;
var isIssueIndex, isMRIndex, page, boardsModel; var isIssueIndex, isMRIndex, page, boardsModel;
var fadeOutLoader = () => { var fadeOutLoader = () => {
...@@ -391,6 +392,10 @@ import CreateLabelDropdown from './create_label'; ...@@ -391,6 +392,10 @@ import CreateLabelDropdown from './create_label';
.then(fadeOutLoader) .then(fadeOutLoader)
.catch(fadeOutLoader); .catch(fadeOutLoader);
} }
else if (handleClick) {
e.preventDefault();
handleClick(label);
}
else { else {
if ($dropdown.hasClass('js-multiselect')) { if ($dropdown.hasClass('js-multiselect')) {
......
...@@ -41,7 +41,6 @@ import './behaviors/'; ...@@ -41,7 +41,6 @@ import './behaviors/';
import './activities'; import './activities';
import './admin'; import './admin';
import './aside'; import './aside';
import './autosave';
import loadAwardsHandler from './awards_handler'; import loadAwardsHandler from './awards_handler';
import bp from './breakpoints'; import bp from './breakpoints';
import './commits'; import './commits';
...@@ -55,7 +54,6 @@ import './gl_dropdown'; ...@@ -55,7 +54,6 @@ import './gl_dropdown';
import './gl_field_error'; import './gl_field_error';
import './gl_field_errors'; import './gl_field_errors';
import './gl_form'; import './gl_form';
import './groups_select';
import './header'; import './header';
import './importer_status'; import './importer_status';
import './issuable_index'; import './issuable_index';
......
...@@ -5,7 +5,7 @@ import _ from 'underscore'; ...@@ -5,7 +5,7 @@ import _ from 'underscore';
(function() { (function() {
this.MilestoneSelect = (function() { this.MilestoneSelect = (function() {
function MilestoneSelect(currentProject, els) { function MilestoneSelect(currentProject, els, options = {}) {
var _this, $els; var _this, $els;
if (currentProject != null) { if (currentProject != null) {
_this = this; _this = this;
...@@ -136,19 +136,26 @@ import _ from 'underscore'; ...@@ -136,19 +136,26 @@ import _ from 'underscore';
}, },
opened: function(e) { opened: function(e) {
const $el = $(e.currentTarget); const $el = $(e.currentTarget);
if ($dropdown.hasClass('js-issue-board-sidebar')) { if ($dropdown.hasClass('js-issue-board-sidebar') || options.handleClick) {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault; selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
} }
$('a.is-active', $el).removeClass('is-active'); $('a.is-active', $el).removeClass('is-active');
$(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active'); $(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
}, },
vue: $dropdown.hasClass('js-issue-board-sidebar'), vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(options) { clicked: function(clickEvent) {
const { $el, e } = options; const { $el, e } = clickEvent;
let selected = options.selectedObj; let selected = clickEvent.selectedObj;
var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore; var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore;
if (!selected) return; if (!selected) return;
if (options.handleClick) {
e.preventDefault();
options.handleClick(selected);
return;
}
page = $('body').attr('data-page'); page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index'); isMRIndex = (page === page && page === 'projects:merge_requests:index');
......
...@@ -5,7 +5,7 @@ default-case, prefer-template, consistent-return, no-alert, no-return-assign, ...@@ -5,7 +5,7 @@ default-case, prefer-template, consistent-return, no-alert, no-return-assign,
no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new,
brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow, brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow,
newline-per-chained-call, no-useless-escape, class-methods-use-this */ newline-per-chained-call, no-useless-escape, class-methods-use-this */
/* global Autosave */
/* global ResolveService */ /* global ResolveService */
/* global mrRefreshWidgetUrl */ /* global mrRefreshWidgetUrl */
...@@ -20,7 +20,7 @@ import Flash from './flash'; ...@@ -20,7 +20,7 @@ import Flash from './flash';
import CommentTypeToggle from './comment_type_toggle'; import CommentTypeToggle from './comment_type_toggle';
import GLForm from './gl_form'; import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler'; import loadAwardsHandler from './awards_handler';
import './autosave'; import Autosave from './autosave';
import TaskList from './task_list'; import TaskList from './task_list';
import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils'; import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index'; import imageDiffHelper from './image_diff/helpers/index';
......
<script> <script>
/* global Autosave */
import { mapActions, mapGetters } from 'vuex'; import { mapActions, mapGetters } from 'vuex';
import _ from 'underscore'; import _ from 'underscore';
import autosize from 'vendor/autosize'; import autosize from 'vendor/autosize';
import Flash from '../../flash'; import Flash from '../../flash';
import '../../autosave'; import Autosave from '../../autosave';
import TaskList from '../../task_list'; import TaskList from '../../task_list';
import * as constants from '../constants'; import * as constants from '../constants';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
......
...@@ -9,8 +9,8 @@ ...@@ -9,8 +9,8 @@
import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
import issueNoteEditedText from './issue_note_edited_text.vue'; import issueNoteEditedText from './issue_note_edited_text.vue';
import issueNoteForm from './issue_note_form.vue'; import issueNoteForm from './issue_note_form.vue';
import placeholderNote from './issue_placeholder_note.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from './issue_placeholder_system_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import autosave from '../mixins/autosave'; import autosave from '../mixins/autosave';
export default { export default {
......
...@@ -5,10 +5,10 @@ ...@@ -5,10 +5,10 @@
import * as constants from '../constants'; import * as constants from '../constants';
import issueNote from './issue_note.vue'; import issueNote from './issue_note.vue';
import issueDiscussion from './issue_discussion.vue'; import issueDiscussion from './issue_discussion.vue';
import issueSystemNote from './issue_system_note.vue'; import systemNote from '../../vue_shared/components/notes/system_note.vue';
import issueCommentForm from './issue_comment_form.vue'; import issueCommentForm from './issue_comment_form.vue';
import placeholderNote from './issue_placeholder_note.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from './issue_placeholder_system_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default { export default {
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
components: { components: {
issueNote, issueNote,
issueDiscussion, issueDiscussion,
issueSystemNote, systemNote,
issueCommentForm, issueCommentForm,
loadingIcon, loadingIcon,
placeholderNote, placeholderNote,
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
} }
return placeholderNote; return placeholderNote;
} else if (note.individual_note) { } else if (note.individual_note) {
return note.notes[0].system ? issueSystemNote : issueNote; return note.notes[0].system ? systemNote : issueNote;
} }
return issueDiscussion; return issueDiscussion;
......
/* globals Autosave */ import Autosave from '../../autosave';
import '../../autosave';
export default { export default {
methods: { methods: {
......
...@@ -6,7 +6,7 @@ import _ from 'underscore'; ...@@ -6,7 +6,7 @@ import _ from 'underscore';
// TODO: remove eventHub hack after code splitting refactor // TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop; window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
function UsersSelect(currentUser, els) { function UsersSelect(currentUser, els, options = {}) {
var $els; var $els;
this.users = this.users.bind(this); this.users = this.users.bind(this);
this.user = this.user.bind(this); this.user = this.user.bind(this);
...@@ -20,6 +20,8 @@ function UsersSelect(currentUser, els) { ...@@ -20,6 +20,8 @@ function UsersSelect(currentUser, els) {
} }
} }
const { handleClick } = options;
$els = $(els); $els = $(els);
if (!els) { if (!els) {
...@@ -442,6 +444,9 @@ function UsersSelect(currentUser, els) { ...@@ -442,6 +444,9 @@ function UsersSelect(currentUser, els) {
} }
if ($el.closest('.add-issues-modal').length) { if ($el.closest('.add-issues-modal').length) {
gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id; gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id;
} else if (handleClick) {
e.preventDefault();
handleClick(user, isMarking);
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
return Issuable.filterResults($dropdown.closest('form')); return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) { } else if ($dropdown.hasClass('js-filter-submit')) {
......
...@@ -286,6 +286,7 @@ export default { ...@@ -286,6 +286,7 @@ export default {
<input <input
id="remove-source-branch-input" id="remove-source-branch-input"
v-model="removeSourceBranch" v-model="removeSourceBranch"
class="js-remove-source-branch-checkbox"
:disabled="isRemoveSourceBranchButtonDisabled" :disabled="isRemoveSourceBranchButtonDisabled"
type="checkbox"/> Remove source branch type="checkbox"/> Remove source branch
</label> </label>
...@@ -311,8 +312,8 @@ export default { ...@@ -311,8 +312,8 @@ export default {
</button> </button>
</template> </template>
<template v-else> <template v-else>
<span class="bold"> <span class="bold js-resolve-mr-widget-items-message">
The pipeline for this merge request has not succeeded yet You can only merge once the items above are resolved
</span> </span>
</template> </template>
</div> </div>
......
...@@ -18,6 +18,12 @@ ...@@ -18,6 +18,12 @@
required: false, required: false,
default: false, default: false,
}, },
class: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
...@@ -25,7 +31,7 @@ ...@@ -25,7 +31,7 @@
return this.inline ? 'span' : 'div'; return this.inline ? 'span' : 'div';
}, },
cssClass() { cssClass() {
return `fa-${this.size}x`; return `fa-${this.size}x ${this.class}`.trim();
}, },
}, },
}; };
......
<script> <script>
/**
* Common component to render a placeholder note and user information.
*
* This component needs to be used with a vuex store.
* That vuex store needs to have a `getUserData` getter that contains
* {
* path: String,
* avatar_url: String,
* name: String,
* username: String,
* }
*
* @example
* <placeholder-note
* :note="{body: 'This is a note'}"
* />
*/
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '../user_avatar/user_avatar_link.vue';
export default { export default {
name: 'issuePlaceholderNote', name: 'placeholderNote',
props: { props: {
note: { note: {
type: Object, type: Object,
......
<script> <script>
/**
* Common component to render a placeholder system note.
*
* @example
* <placeholder-system-note
* :note="{ body: 'Commands are being applied'}"
* />
*/
export default { export default {
name: 'placeholderSystemNote', name: 'placeholderSystemNote',
props: { props: {
......
<script> <script>
/**
* Common component to render a system note, icon and user information.
*
* This component needs to be used with a vuex store.
* That vuex store needs to have a `targetNoteHash` getter
*
* @example
* <system-note
* :note="{
* id: String,
* author: Object,
* createdAt: String,
* note_html: String,
* system_note_icon_name: String
* }"
* />
*/
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import issueNoteHeader from './issue_note_header.vue'; import issueNoteHeader from '../../../notes/components/issue_note_header.vue';
import { spriteIcon } from '../../../lib/utils/common_utils';
export default { export default {
name: 'systemNote', name: 'systemNote',
...@@ -24,7 +42,7 @@ ...@@ -24,7 +42,7 @@
return this.targetNoteHash === this.noteAnchorId; return this.targetNoteHash === this.noteAnchorId;
}, },
iconHtml() { iconHtml() {
return gl.utils.spriteIcon(this.note.system_note_icon_name); return spriteIcon(this.note.system_note_icon_name);
}, },
}, },
}; };
...@@ -46,7 +64,8 @@ ...@@ -46,7 +64,8 @@
:author="note.author" :author="note.author"
:created-at="note.created_at" :created-at="note.created_at"
:note-id="note.id" :note-id="note.id"
:action-text-html="note.note_html" /> :action-text-html="note.note_html"
/>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -5,17 +5,27 @@ export default { ...@@ -5,17 +5,27 @@ export default {
props: { props: {
title: { title: {
type: String, type: String,
required: true, required: false,
}, },
text: { text: {
type: String, type: String,
required: false, required: false,
}, },
hideFooter: {
type: Boolean,
required: false,
default: false,
},
kind: { kind: {
type: String, type: String,
required: false, required: false,
default: 'primary', default: 'primary',
}, },
modalDialogClass: {
type: String,
required: false,
default: '',
},
closeKind: { closeKind: {
type: String, type: String,
required: false, required: false,
...@@ -30,6 +40,11 @@ export default { ...@@ -30,6 +40,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
submitDisabled: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
...@@ -57,43 +72,57 @@ export default { ...@@ -57,43 +72,57 @@ export default {
</script> </script>
<template> <template>
<div <div class="modal-open">
class="modal popup-dialog" <div
role="dialog" class="modal popup-dialog"
tabindex="-1"> role="dialog"
<div class="modal-dialog" role="document"> tabindex="-1"
<div class="modal-content"> >
<div class="modal-header"> <div
<button type="button" :class="modalDialogClass"
class="close" class="modal-dialog"
@click="close" role="document"
aria-label="Close"> >
<span aria-hidden="true">&times;</span> <div class="modal-content">
</button> <div class="modal-header">
<h4 class="modal-title">{{this.title}}</h4> <slot name="header">
</div> <h4 class="modal-title pull-left">
<div class="modal-body"> {{this.title}}
<slot name="body" :text="text"> </h4>
<p>{{text}}</p> <button
</slot> type="button"
</div> class="close pull-right"
<div class="modal-footer"> @click="close"
<button aria-label="Close"
type="button" >
class="btn" <span aria-hidden="true">&times;</span>
:class="btnCancelKindClass" </button>
@click="close"> </slot>
{{ closeButtonLabel }} </div>
</button> <div class="modal-body">
<button <slot name="body" :text="text">
type="button" <p>{{this.text}}</p>
class="btn" </slot>
:class="btnKindClass" </div>
@click="emitSubmit(true)"> <div class="modal-footer" v-if="!hideFooter">
{{ primaryButtonLabel }} <button
</button> type="button"
class="btn pull-left"
:class="btnCancelKindClass"
@click="close">
{{ closeButtonLabel }}
</button>
<button
type="button"
class="btn pull-right"
:class="btnKindClass"
@click="emitSubmit(true)">
{{ primaryButtonLabel }}
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="modal-backdrop fade in" />
</div> </div>
</template> </template>
...@@ -12,12 +12,14 @@ ...@@ -12,12 +12,14 @@
:img-alt="tooltipText" :img-alt="tooltipText"
:img-size="20" :img-size="20"
:tooltip-text="tooltipText" :tooltip-text="tooltipText"
tooltip-placement="top" :tooltip-placement="top"
:username="username"
/> />
*/ */
import userAvatarImage from './user_avatar_image.vue'; import userAvatarImage from './user_avatar_image.vue';
import tooltip from '../../directives/tooltip';
export default { export default {
name: 'UserAvatarLink', name: 'UserAvatarLink',
...@@ -60,6 +62,22 @@ export default { ...@@ -60,6 +62,22 @@ export default {
required: false, required: false,
default: 'top', default: 'top',
}, },
username: {
type: String,
required: false,
default: '',
},
},
computed: {
shouldShowUsername() {
return this.username.length > 0;
},
avatarTooltipText() {
return this.shouldShowUsername ? '' : this.tooltipText;
},
},
directives: {
tooltip,
}, },
}; };
</script> </script>
...@@ -73,8 +91,13 @@ export default { ...@@ -73,8 +91,13 @@ export default {
:img-alt="imgAlt" :img-alt="imgAlt"
:css-classes="imgCssClasses" :css-classes="imgCssClasses"
:size="imgSize" :size="imgSize"
:tooltip-text="tooltipText" :tooltip-text="avatarTooltipText"
:tooltip-placement="tooltipPlacement"
/><span
v-if="shouldShowUsername"
v-tooltip
:title="tooltipText"
:tooltip-placement="tooltipPlacement" :tooltip-placement="tooltipPlacement"
/> >{{username}}</span>
</a> </a>
</template> </template>
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
.cred { color: $common-red; } .cred { color: $common-red; }
.cgreen { color: $common-green; } .cgreen { color: $common-green; }
.cdark { color: $common-gray-dark; } .cdark { color: $common-gray-dark; }
.text-secondary {
color: $gl-text-color-secondary;
}
/** COMMON CLASSES **/ /** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; } .prepend-top-0 { margin-top: 0; }
......
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
.dropdown-menu-nav { .dropdown-menu-nav {
@include set-visible; @include set-visible;
display: block; display: block;
min-height: 40px;
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
width: 100%; width: 100%;
......
...@@ -65,7 +65,7 @@ ...@@ -65,7 +65,7 @@
display: flex; display: flex;
flex: 1; flex: 1;
-webkit-flex: 1; -webkit-flex: 1;
padding-left: 30px; padding-left: 12px;
position: relative; position: relative;
margin-bottom: 0; margin-bottom: 0;
} }
...@@ -221,10 +221,6 @@ ...@@ -221,10 +221,6 @@
box-shadow: 0 0 4px $search-input-focus-shadow-color; box-shadow: 0 0 4px $search-input-focus-shadow-color;
} }
&.focus .fa-filter {
color: $common-gray-dark;
}
gl-emoji { gl-emoji {
display: inline-block; display: inline-block;
font-family: inherit; font-family: inherit;
...@@ -251,13 +247,6 @@ ...@@ -251,13 +247,6 @@
} }
} }
.fa-filter {
position: absolute;
top: 10px;
left: 10px;
color: $gray-darkest;
}
.fa-times { .fa-times {
right: 10px; right: 10px;
color: $gray-darkest; color: $gray-darkest;
......
...@@ -42,3 +42,11 @@ body.modal-open { ...@@ -42,3 +42,11 @@ body.modal-open {
width: 98%; width: 98%;
} }
} }
.modal.popup-dialog {
display: block;
}
.modal-body {
background-color: $modal-body-bg;
}
...@@ -164,3 +164,36 @@ $pre-border-color: $border-color; ...@@ -164,3 +164,36 @@ $pre-border-color: $border-color;
$table-bg-accent: $gray-light; $table-bg-accent: $gray-light;
$zindex-popover: 900; $zindex-popover: 900;
//== Modals
//
//##
//** Padding applied to the modal body
$modal-inner-padding: $gl-padding;
//** Padding applied to the modal title
$modal-title-padding: $gl-padding;
//** Modal title line-height
// $modal-title-line-height: $line-height-base
//** Background color of modal content area
$modal-content-bg: $gray-light;
$modal-body-bg: $white-light;
//** Modal content border color
// $modal-content-border-color: rgba(0,0,0,.2)
//** Modal content border color **for IE8**
// $modal-content-fallback-border-color: #999
//** Modal backdrop background color
// $modal-backdrop-bg: #000
//** Modal backdrop opacity
// $modal-backdrop-opacity: .5
//** Modal header border color
// $modal-header-border-color: #e5e5e5
//** Modal footer border color
// $modal-footer-border-color: $modal-header-border-color
// $modal-lg: 900px
// $modal-md: 600px
// $modal-sm: 300px
.modal.popup-dialog {
display: block;
background-color: $black-transparent;
z-index: 2100;
@media (min-width: $screen-md-min) {
.modal-dialog {
width: 600px;
margin: 30px auto;
}
}
}
.project-refs-form, .project-refs-form,
.project-refs-target-form { .project-refs-target-form {
display: inline-block; display: inline-block;
......
...@@ -10,7 +10,7 @@ class ConfirmationsController < Devise::ConfirmationsController ...@@ -10,7 +10,7 @@ class ConfirmationsController < Devise::ConfirmationsController
users_almost_there_path users_almost_there_path
end end
def after_confirmation_path_for(_resource_name, resource) def after_confirmation_path_for(resource_name, resource)
# incoming resource can either be a :user or an :email # incoming resource can either be a :user or an :email
if signed_in?(:user) if signed_in?(:user)
after_sign_in(resource) after_sign_in(resource)
......
...@@ -57,6 +57,10 @@ class HelpController < ApplicationController ...@@ -57,6 +57,10 @@ class HelpController < ApplicationController
def shortcuts def shortcuts
end end
def instance_configuration
@instance_configuration = InstanceConfiguration.new
end
def ui def ui
@user = User.new(id: 0, name: 'John Doe', username: '@johndoe') @user = User.new(id: 0, name: 'John Doe', username: '@johndoe')
end end
......
...@@ -15,6 +15,8 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -15,6 +15,8 @@ class Projects::BranchesController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
@refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name)) @refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
@merged_branch_names =
repository.merged_branch_names(@branches.map(&:name))
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37429 # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37429
Gitlab::GitalyClient.allow_n_plus_1_calls do Gitlab::GitalyClient.allow_n_plus_1_calls do
@max_commits = @branches.reduce(0) do |memo, branch| @max_commits = @branches.reduce(0) do |memo, branch|
......
...@@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_create_issue!, only: [:new, :create] before_action :authorize_create_issue!, only: [:new, :create]
# Allow modify issue # Allow modify issue
before_action :authorize_update_issue!, only: [:update, :move] before_action :authorize_update_issue!, only: [:edit, :update, :move]
# Allow create a new branch and empty WIP merge request from current issue # Allow create a new branch and empty WIP merge request from current issue
before_action :authorize_create_merge_request!, only: [:create_merge_request] before_action :authorize_create_merge_request!, only: [:create_merge_request]
...@@ -63,6 +63,10 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -63,6 +63,10 @@ class Projects::IssuesController < Projects::ApplicationController
respond_with(@issue) respond_with(@issue)
end end
def edit
respond_with(@issue)
end
def show def show
@noteable = @issue @noteable = @issue
@note = @project.notes.new(noteable: @issue) @note = @project.notes.new(noteable: @issue)
...@@ -122,6 +126,10 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -122,6 +126,10 @@ class Projects::IssuesController < Projects::ApplicationController
@issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue) @issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue)
respond_to do |format| respond_to do |format|
format.html do
recaptcha_check_with_fallback { render :edit }
end
format.json do format.json do
render_issue_json render_issue_json
end end
......
...@@ -23,7 +23,7 @@ class BranchesFinder ...@@ -23,7 +23,7 @@ class BranchesFinder
def filter_by_name(branches) def filter_by_name(branches)
if search if search
branches.select { |branch| branch.name.include?(search) } branches.select { |branch| branch.name.upcase.include?(search.upcase) }
else else
branches branches
end end
......
...@@ -20,17 +20,6 @@ module BoardsHelper ...@@ -20,17 +20,6 @@ module BoardsHelper
project_issues_path(@project) project_issues_path(@project)
end end
def current_board_json
board = @board || @boards.first
board.to_json(
only: [:id, :name, :milestone_id],
include: {
milestone: { only: [:title] }
}
)
end
def board_base_url def board_base_url
project_boards_path(@project) project_boards_path(@project)
end end
......
module InstanceConfigurationHelper
def instance_configuration_cell_html(value, &block)
return '-' unless value.to_s.presence
block_given? ? yield(value) : value
end
def instance_configuration_host(host)
@instance_configuration_host ||= instance_configuration_cell_html(host).capitalize
end
# Value must be in bytes
def instance_configuration_human_size_cell(value)
instance_configuration_cell_html(value) do |v|
number_to_human_size(v, strip_insignificant_zeros: true, significant: false)
end
end
end
...@@ -249,9 +249,7 @@ module Ci ...@@ -249,9 +249,7 @@ module Ci
end end
def commit def commit
@commit ||= project.commit(sha) @commit ||= project.commit_by(oid: sha)
rescue
nil
end end
def branch? def branch?
......
...@@ -14,6 +14,8 @@ class Email < ActiveRecord::Base ...@@ -14,6 +14,8 @@ class Email < ActiveRecord::Base
devise :confirmable devise :confirmable
self.reconfirmable = false # currently email can't be changed, no need to reconfirm self.reconfirmable = false # currently email can't be changed, no need to reconfirm
delegate :username, to: :user
def email=(value) def email=(value)
write_attribute(:email, value.downcase.strip) write_attribute(:email, value.downcase.strip)
end end
......
require 'resolv'
class InstanceConfiguration
SSH_ALGORITHMS = %w(DSA ECDSA ED25519 RSA).freeze
SSH_ALGORITHMS_PATH = '/etc/ssh/'.freeze
CACHE_KEY = 'instance_configuration'.freeze
EXPIRATION_TIME = 24.hours
def settings
@configuration ||= Rails.cache.fetch(CACHE_KEY, expires_in: EXPIRATION_TIME) do
{ ssh_algorithms_hashes: ssh_algorithms_hashes,
host: host,
gitlab_pages: gitlab_pages,
gitlab_ci: gitlab_ci }.deep_symbolize_keys
end
end
private
def ssh_algorithms_hashes
SSH_ALGORITHMS.map { |algo| ssh_algorithm_hashes(algo) }.compact
end
def host
Settings.gitlab.host
end
def gitlab_pages
Settings.pages.to_h.merge(ip_address: resolv_dns(Settings.pages.host))
end
def resolv_dns(dns)
Resolv.getaddress(dns)
rescue Resolv::ResolvError
end
def gitlab_ci
Settings.gitlab_ci
.to_h
.merge(artifacts_max_size: { value: Settings.artifacts.max_size&.megabytes,
default: 100.megabytes })
end
def ssh_algorithm_file(algorithm)
File.join(SSH_ALGORITHMS_PATH, "ssh_host_#{algorithm.downcase}_key.pub")
end
def ssh_algorithm_hashes(algorithm)
content = ssh_algorithm_file_content(algorithm)
return unless content.present?
{ name: algorithm,
md5: ssh_algorithm_md5(content),
sha256: ssh_algorithm_sha256(content) }
end
def ssh_algorithm_file_content(algorithm)
file = ssh_algorithm_file(algorithm)
return unless File.exist?(file)
File.read(file)
end
def ssh_algorithm_md5(ssh_file_content)
OpenSSL::Digest::MD5.hexdigest(ssh_file_content).scan(/../).join(':')
end
def ssh_algorithm_sha256(ssh_file_content)
OpenSSL::Digest::SHA256.hexdigest(ssh_file_content)
end
end
...@@ -396,6 +396,10 @@ class MergeRequest < ActiveRecord::Base ...@@ -396,6 +396,10 @@ class MergeRequest < ActiveRecord::Base
end end
def merge_ongoing? def merge_ongoing?
# While the MergeRequest is locked, it should present itself as 'merge ongoing'.
# The unlocking process is handled by StuckMergeJobsWorker scheduled in Cron.
return true if locked?
!!merge_jid && !merged? && Gitlab::SidekiqStatus.running?(merge_jid) !!merge_jid && !merged? && Gitlab::SidekiqStatus.running?(merge_jid)
end end
......
...@@ -540,6 +540,10 @@ class Project < ActiveRecord::Base ...@@ -540,6 +540,10 @@ class Project < ActiveRecord::Base
repository.commit(ref) repository.commit(ref)
end end
def commit_by(oid:)
repository.commit_by(oid: oid)
end
# ref can't be HEAD, can only be branch/tag name or SHA # ref can't be HEAD, can only be branch/tag name or SHA
def latest_successful_builds_for(ref = default_branch) def latest_successful_builds_for(ref = default_branch)
latest_pipeline = pipelines.latest_successful_for(ref) latest_pipeline = pipelines.latest_successful_for(ref)
...@@ -553,7 +557,7 @@ class Project < ActiveRecord::Base ...@@ -553,7 +557,7 @@ class Project < ActiveRecord::Base
def merge_base_commit(first_commit_id, second_commit_id) def merge_base_commit(first_commit_id, second_commit_id)
sha = repository.merge_base(first_commit_id, second_commit_id) sha = repository.merge_base(first_commit_id, second_commit_id)
repository.commit(sha) if sha commit_by(oid: sha) if sha
end end
def saved? def saved?
......
...@@ -3,6 +3,8 @@ class JiraService < IssueTrackerService ...@@ -3,6 +3,8 @@ class JiraService < IssueTrackerService
validates :url, url: true, presence: true, if: :activated? validates :url, url: true, presence: true, if: :activated?
validates :api_url, url: true, allow_blank: true validates :api_url, url: true, allow_blank: true
validates :username, presence: true, if: :activated?
validates :password, presence: true, if: :activated?
prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id, :title, :description prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id, :title, :description
......
...@@ -153,7 +153,10 @@ class KubernetesService < DeploymentService ...@@ -153,7 +153,10 @@ class KubernetesService < DeploymentService
end end
def default_namespace def default_namespace
"#{project.path}-#{project.id}" if project.present? return unless project
slug = "#{project.path}-#{project.id}".downcase
slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end end
def build_kubeclient!(api_path: 'api', api_version: 'v1') def build_kubeclient!(api_path: 'api', api_version: 'v1')
......
...@@ -76,6 +76,7 @@ class Repository ...@@ -76,6 +76,7 @@ class Repository
@full_path = full_path @full_path = full_path
@disk_path = disk_path || full_path @disk_path = disk_path || full_path
@project = project @project = project
@commit_cache = {}
end end
def ==(other) def ==(other)
...@@ -103,18 +104,17 @@ class Repository ...@@ -103,18 +104,17 @@ class Repository
def commit(ref = 'HEAD') def commit(ref = 'HEAD')
return nil unless exists? return nil unless exists?
return ref if ref.is_a?(::Commit)
commit = find_commit(ref)
if ref.is_a?(Gitlab::Git::Commit) end
ref
else
Gitlab::Git::Commit.find(raw_repository, ref)
end
commit = ::Commit.new(commit, @project) if commit # Finding a commit by the passed SHA
commit # Also takes care of caching, based on the SHA
rescue Rugged::OdbError, Rugged::TreeError def commit_by(oid:)
nil return @commit_cache[oid] if @commit_cache.key?(oid)
@commit_cache[oid] = find_commit(oid)
end end
def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil) def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil)
...@@ -231,7 +231,7 @@ class Repository ...@@ -231,7 +231,7 @@ class Repository
# branches or tags, but we want to keep some of these commits around, for # branches or tags, but we want to keep some of these commits around, for
# example if they have comments or CI builds. # example if they have comments or CI builds.
def keep_around(sha) def keep_around(sha)
return unless sha && commit(sha) return unless sha && commit_by(oid: sha)
return if kept_around?(sha) return if kept_around?(sha)
...@@ -862,22 +862,12 @@ class Repository ...@@ -862,22 +862,12 @@ class Repository
end end
def ff_merge(user, source, target_branch, merge_request: nil) def ff_merge(user, source, target_branch, merge_request: nil)
our_commit = rugged.branches[target_branch].target their_commit_id = commit(source)&.id
their_commit = raise 'Invalid merge source' if their_commit_id.nil?
if source.is_a?(Gitlab::Git::Commit)
source.raw_commit
else
rugged.lookup(source)
end
raise 'Invalid merge target' if our_commit.nil?
raise 'Invalid merge source' if their_commit.nil?
with_branch(user, target_branch) do |start_commit| merge_request&.update(in_progress_merge_commit_sha: their_commit_id)
merge_request&.update(in_progress_merge_commit_sha: their_commit.oid)
their_commit.oid with_cache_hooks { raw.ff_merge(user, their_commit_id, target_branch) }
end
end end
def revert( def revert(
...@@ -912,18 +902,27 @@ class Repository ...@@ -912,18 +902,27 @@ class Repository
end end
end end
def merged_to_root_ref?(branch_name) def merged_to_root_ref?(branch_or_name, pre_loaded_merged_branches = nil)
branch_commit = commit(branch_name) branch = Gitlab::Git::Branch.find(self, branch_or_name)
root_ref_commit = commit(root_ref)
if branch_commit if branch
same_head = branch_commit.id == root_ref_commit.id root_ref_sha = commit(root_ref).sha
!same_head && ancestor?(branch_commit.id, root_ref_commit.id) same_head = branch.target == root_ref_sha
merged =
if pre_loaded_merged_branches
pre_loaded_merged_branches.include?(branch.name)
else
ancestor?(branch.target, root_ref_sha)
end
!same_head && merged
else else
nil nil
end end
end end
delegate :merged_branch_names, to: :raw_repository
def merge_base(first_commit_id, second_commit_id) def merge_base(first_commit_id, second_commit_id)
first_commit_id = commit(first_commit_id).try(:id) || first_commit_id first_commit_id = commit(first_commit_id).try(:id) || first_commit_id
second_commit_id = commit(second_commit_id).try(:id) || second_commit_id second_commit_id = commit(second_commit_id).try(:id) || second_commit_id
...@@ -1031,6 +1030,10 @@ class Repository ...@@ -1031,6 +1030,10 @@ class Repository
if instance_variable_defined?(ivar) if instance_variable_defined?(ivar)
instance_variable_get(ivar) instance_variable_get(ivar)
else else
# If the repository doesn't exist and a fallback was specified we return
# that value inmediately. This saves us Rugged/gRPC invocations.
return fallback unless fallback.nil? || exists?
begin begin
value = value =
if memoize_only if memoize_only
...@@ -1040,8 +1043,9 @@ class Repository ...@@ -1040,8 +1043,9 @@ class Repository
end end
instance_variable_set(ivar, value) instance_variable_set(ivar, value)
rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository
# if e.g. HEAD or the entire repository doesn't exist we want to # Even if the above `#exists?` check passes these errors might still
# gracefully handle this and not cache anything. # occur (for example because of a non-existing HEAD). We want to
# gracefully handle this and not cache anything
fallback fallback
end end
end end
...@@ -1069,6 +1073,18 @@ class Repository ...@@ -1069,6 +1073,18 @@ class Repository
private private
# TODO Generice finder, later split this on finders by Ref or Oid
# gitlab-org/gitlab-ce#39239
def find_commit(oid_or_ref)
commit = if oid_or_ref.is_a?(Gitlab::Git::Commit)
oid_or_ref
else
Gitlab::Git::Commit.find(raw_repository, oid_or_ref)
end
::Commit.new(commit, @project) if commit
end
def blob_data_at(sha, path) def blob_data_at(sha, path)
blob = blob_at(sha, path) blob = blob_at(sha, path)
return unless blob return unless blob
...@@ -1107,12 +1123,12 @@ class Repository ...@@ -1107,12 +1123,12 @@ class Repository
def last_commit_for_path_by_gitaly(sha, path) def last_commit_for_path_by_gitaly(sha, path)
c = raw_repository.gitaly_commit_client.last_commit_for_path(sha, path) c = raw_repository.gitaly_commit_client.last_commit_for_path(sha, path)
commit(c) commit_by(oid: c)
end end
def last_commit_for_path_by_rugged(sha, path) def last_commit_for_path_by_rugged(sha, path)
sha = last_commit_id_for_path_by_shelling_out(sha, path) sha = last_commit_id_for_path_by_shelling_out(sha, path)
commit(sha) commit_by(oid: sha)
end end
def last_commit_id_for_path_by_shelling_out(sha, path) def last_commit_id_for_path_by_shelling_out(sha, path)
......
...@@ -82,16 +82,9 @@ module MergeRequests ...@@ -82,16 +82,9 @@ module MergeRequests
@merge_request.can_remove_source_branch?(branch_deletion_user) @merge_request.can_remove_source_branch?(branch_deletion_user)
end end
# Logs merge error message and cleans `MergeRequest#merge_jid`.
#
def handle_merge_error(log_message:, save_message_on_model: false) def handle_merge_error(log_message:, save_message_on_model: false)
Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{log_message}") Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{log_message}")
@merge_request.update(merge_error: log_message) if save_message_on_model
if save_message_on_model
@merge_request.update(merge_error: log_message, merge_jid: nil)
else
clean_merge_jid
end
end end
def merge_request_info def merge_request_info
......
...@@ -83,7 +83,7 @@ class SystemHooksService ...@@ -83,7 +83,7 @@ class SystemHooksService
project_id: model.id, project_id: model.id,
owner_name: owner.name, owner_name: owner.name,
owner_email: owner.respond_to?(:email) ? owner.email : "", owner_email: owner.respond_to?(:email) ? owner.email : "",
project_visibility: Project.visibility_levels.key(model.visibility_level_value).downcase project_visibility: model.visibility.downcase
} }
end end
......
...@@ -6,13 +6,13 @@ ...@@ -6,13 +6,13 @@
.fade-right= icon('angle-right') .fade-right= icon('angle-right')
%ul.nav-links.scrolling-tabs %ul.nav-links.scrolling-tabs
= nav_link(page: [dashboard_projects_path, root_path]) do = nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do = link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your projects Your projects
= nav_link(page: starred_dashboard_projects_path) do = nav_link(page: starred_dashboard_projects_path) do
= link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do = link_to starred_dashboard_projects_path, data: {placement: 'right'} do
Starred projects Starred projects
= nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do
= link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do = link_to explore_root_path, data: {placement: 'right'} do
Explore projects Explore projects
.nav-controls .nav-controls
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
%span= Gitlab::VERSION %span= Gitlab::VERSION
%small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION) %small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION)
= version_status_badge = version_status_badge
%p.slead %p.slead
GitLab is open source software to collaborate on code. GitLab is open source software to collaborate on code.
%br %br
...@@ -23,6 +24,7 @@ ...@@ -23,6 +24,7 @@
Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises. Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises.
%br %br
Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}. Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}.
%p= link_to 'Check the current instance configuration ', help_instance_configuration_url
%hr %hr
.row.prepend-top-default .row.prepend-top-default
......
- page_title 'Instance Configuration'
.wiki.documentation
%h1 Instance Configuration
%p
In this page you will find information about the settings that are used in your current instance.
= render 'help/instance_configuration/ssh_info'
= render 'help/instance_configuration/gitlab_pages'
= render 'help/instance_configuration/gitlab_ci'
%p
%strong Table of contents
%ul
= content_for :table_content
= content_for :settings_content
- content_for :table_content do
%li= link_to 'GitLab CI', '#gitlab-ci'
- content_for :settings_content do
%h2#gitlab-ci
GitLab CI
%p
Below are the current settings regarding
= succeed('.') { link_to('GitLab CI', 'https://about.gitlab.com/gitlab-ci', target: '_blank') }
.table-responsive
%table
%thead
%tr
%th Setting
%th= instance_configuration_host(@instance_configuration.settings[:host])
%th Default
%tbody
%tr
- artifacts_size = @instance_configuration.settings[:gitlab_ci][:artifacts_max_size]
%td Artifacts maximum size
%td= instance_configuration_human_size_cell(artifacts_size[:value])
%td= instance_configuration_human_size_cell(artifacts_size[:default])
- gitlab_pages = @instance_configuration.settings[:gitlab_pages]
- content_for :table_content do
%li= link_to 'GitLab Pages', '#gitlab-pages'
- content_for :settings_content do
%h2#gitlab-pages
GitLab Pages
%p
Below are the settings for
= succeed('.') { link_to('Gitlab Pages', gitlab_pages[:url], target: '_blank') }
.table-responsive
%table
%thead
%tr
%th Setting
%th= instance_configuration_host(@instance_configuration.settings[:host])
%tbody
%tr
%td Domain Name
%td
%code= instance_configuration_cell_html(gitlab_pages[:host])
%tr
%td IP Address
%td
%code= instance_configuration_cell_html(gitlab_pages[:ip_address])
%tr
%td Port
%td
%code= instance_configuration_cell_html(gitlab_pages[:port])
%br
%p
The maximum size of your Pages site is regulated by the artifacts maximum
size which is part of #{succeed('.') { link_to('GitLab CI', '#gitlab-ci') }}
- ssh_info = @instance_configuration.settings[:ssh_algorithms_hashes]
- if ssh_info.any?
- content_for :table_content do
%li= link_to 'SSH host keys fingerprints', '#ssh-host-keys-fingerprints'
- content_for :settings_content do
%h2#ssh-host-keys-fingerprints
SSH host keys fingerprints
%p
Below are the fingerprints for the current instance SSH host keys.
.table-responsive
%table
%thead
%tr
%th Algorithm
%th MD5
%th SHA256
%tbody
- ssh_info.each do |algorithm|
%tr
%td= algorithm[:name]
%td
%code= instance_configuration_cell_html(algorithm[:md5])
%td
%code= instance_configuration_cell_html(algorithm[:sha256])
- merged = local_assigns.fetch(:merged, false)
- commit = @repository.commit(branch.dereferenced_target) - commit = @repository.commit(branch.dereferenced_target)
- bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0 - bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0
- diverging_commit_counts = @repository.diverging_commit_counts(branch) - diverging_commit_counts = @repository.diverging_commit_counts(branch)
...@@ -12,7 +13,7 @@ ...@@ -12,7 +13,7 @@
&nbsp; &nbsp;
- if branch.name == @repository.root_ref - if branch.name == @repository.root_ref
%span.label.label-primary default %span.label.label-primary default
- elsif @repository.merged_to_root_ref? branch.name - elsif merged
%span.label.label-info.has-tooltip{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } } %span.label.label-info.has-tooltip{ title: s_('Branches|Merged into %{default_branch}') % { default_branch: @repository.root_ref } }
= s_('Branches|merged') = s_('Branches|merged')
...@@ -47,7 +48,7 @@ ...@@ -47,7 +48,7 @@
target: "#modal-delete-branch", target: "#modal-delete-branch",
delete_path: project_branch_path(@project, branch.name), delete_path: project_branch_path(@project, branch.name),
branch_name: branch.name, branch_name: branch.name,
is_merged: ("true" if @repository.merged_to_root_ref?(branch.name)) } } is_merged: ("true" if merged) } }
= icon("trash-o") = icon("trash-o")
- else - else
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled", %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
......
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
- if @branches.any? - if @branches.any?
%ul.content-list.all-branches %ul.content-list.all-branches
- @branches.each do |branch| - @branches.each do |branch|
= render "projects/branches/branch", branch: branch = render "projects/branches/branch", branch: branch, merged: @repository.merged_to_root_ref?(branch, @merged_branch_names)
= paginate @branches, theme: 'gitlab' = paginate @branches, theme: 'gitlab'
- else - else
.nothing-here-block .nothing-here-block
......
- page_title "Edit", "#{@issue.title} (#{@issue.to_reference})", "Issues"
%h3.page-title
Edit Issue ##{@issue.iid}
%hr
= render "form"
...@@ -25,7 +25,6 @@ ...@@ -25,7 +25,6 @@
%ul.tokens-container.list-unstyled %ul.tokens-container.list-unstyled
%li.input-token %li.input-token
%input.form-control.filtered-search{ search_filter_input_options(type) } %input.form-control.filtered-search{ search_filter_input_options(type) }
= icon('filter')
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } } %ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } } %li.filter-dropdown-item{ data: { action: 'submit' } }
......
...@@ -23,7 +23,7 @@ class StuckMergeJobsWorker ...@@ -23,7 +23,7 @@ class StuckMergeJobsWorker
merge_requests = MergeRequest.where(id: completed_ids) merge_requests = MergeRequest.where(id: completed_ids)
merge_requests.where.not(merge_commit_sha: nil).update_all(state: :merged) merge_requests.where.not(merge_commit_sha: nil).update_all(state: :merged)
merge_requests.where(merge_commit_sha: nil).update_all(state: :opened) merge_requests.where(merge_commit_sha: nil).update_all(state: :opened, merge_jid: nil)
Rails.logger.info("Updated state of locked merge jobs. JIDs: #{completed_jids.join(', ')}") Rails.logger.info("Updated state of locked merge jobs. JIDs: #{completed_jids.join(', ')}")
end end
......
---
title: Remove filter icon from search bar
merge_request:
author:
type: other
---
title: Case insensitive search for branches
merge_request: 14995
author: George Andrinopoulos
type: fixed
---
title: Moves placeholders components into shared folder with documentation. Makes
them easier to reuse in MR and Snippets comments
merge_request:
author:
type: other
---
title: Update default disabled merge request widget message to reflect a general failure
merge_request: 14960
author:
type: changed
---
title: 'Fix bug preventing secondary emails from being confirmed'
merge_request: 15010
author:
type: fixed
---
title: Remove overzealous tooltips in projects page tabs
merge_request: 15017
author:
type: removed
---
title: Fix editing issue description in mobile view
merge_request:
author:
type: fixed
---
title: Fix bitbucket login
merge_request: 15051
author:
type: fixed
---
title: Validate username/pw for Jiraservice, require them in the API
merge_request: 15025
author: Robert Schilling
type: fixed
---
title: Use the correct visibility attribute for projects in system hooks
merge_request: 15065
author:
type: fixed
---
title: Automatic configuration settings page
merge_request: 13850
author: Francisco Lopez
type: added
---
title: Fix broken wiki pages that link to a wiki file
merge_request: 15019
author:
type: fixed
---
title: Fix widget of locked merge requests not being presented
merge_request:
author:
type: fixed
---
title: Auto Devops kubernetes default namespace is now correctly built out of gitlab
project group-name
merge_request: 14642
author: Mircea Danila Dumitrescu
type: fixed
---
title: Improve branch listing page performance
merge_request: 14729
author:
type: performance
---
title: Cache commits fetched from the repository
merge_request:
author:
type: performance
get 'help' => 'help#index' get 'help' => 'help#index'
get 'help/shortcuts' => 'help#shortcuts' get 'help/shortcuts' => 'help#shortcuts'
get 'help/ui' => 'help#ui' get 'help/ui' => 'help#ui'
get 'help/*path' => 'help#show', as: :help_page get 'help/instance_configuration' => 'help#instance_configuration'
get 'help/*path' => 'help#show', as: :help_page
...@@ -478,8 +478,8 @@ PUT /projects/:id/services/jira ...@@ -478,8 +478,8 @@ PUT /projects/:id/services/jira
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project, e.g., `https://jira.example.com`. | | `url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project, e.g., `https://jira.example.com`. |
| `project_key` | string | yes | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. | | `project_key` | string | yes | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. |
| `username` | string | no | The username of the user created to be used with GitLab/JIRA. | | `username` | string | yes | The username of the user created to be used with GitLab/JIRA. |
| `password` | string | no | The password of the user created to be used with GitLab/JIRA. | | `password` | string | yes | The password of the user created to be used with GitLab/JIRA. |
| `jira_issue_transition_id` | integer | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. | | `jira_issue_transition_id` | integer | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. |
### Delete JIRA service ### Delete JIRA service
......
This diff is collapsed.
...@@ -3,31 +3,28 @@ ...@@ -3,31 +3,28 @@
Merge requests are useful to integrate separate changes that you've made to a Merge requests are useful to integrate separate changes that you've made to a
project, on different branches. This is a brief guide on how to create a merge project, on different branches. This is a brief guide on how to create a merge
request. For more information, check the request. For more information, check the
[merge requests documentation](../user/project/merge_requests.md). [merge requests documentation](../user/project/merge_requests/index.md).
--- ---
1. Before you start, you should have already [created a branch](create-branch.md) 1. Before you start, you should have already [created a branch](create-branch.md)
and [pushed your changes](basic-git-commands.md) to GitLab. and [pushed your changes](basic-git-commands.md) to GitLab.
1. Go to the project where you'd like to merge your changes and click on the
1. You can then go to the project where you'd like to merge your changes and **Merge requests** tab.
click on the **Merge requests** tab.
![Merge requests](img/project_navbar.png)
1. Click on **New merge request** on the right side of the screen. 1. Click on **New merge request** on the right side of the screen.
1. From there on, you have the option to select the source branch and the target
![New Merge Request](img/merge_request_new.png) branch you'd like to compare to. The default target project is the upstream
repository, but you can choose to compare across any of its forks.
1. Select a source branch and click on the **Compare branches and continue** button.
![Select a branch](img/merge_request_select_branch.png) ![Select a branch](img/merge_request_select_branch.png)
1. When ready, click on the **Compare branches and continue** button.
1. At a minimum, add a title and a description to your merge request. Optionally, 1. At a minimum, add a title and a description to your merge request. Optionally,
select a user to review your merge request and to accept or close it. You may select a user to review your merge request and to accept or close it. You may
also select a milestone and labels. also select a milestone and labels.
![New merge request page](img/merge_request_page.png) ![New merge request page](img/merge_request_page.png)
1. When ready, click on the **Submit merge request** button. Your merge request 1. When ready, click on the **Submit merge request** button.
will be ready to be approved and published.
Your merge request will be ready to be approved and merged.
...@@ -132,14 +132,17 @@ On the sign in page there should now be a SAML button below the regular sign in ...@@ -132,14 +132,17 @@ On the sign in page there should now be a SAML button below the regular sign in
Click the icon to begin the authentication process. If everything goes well the user Click the icon to begin the authentication process. If everything goes well the user
will be returned to GitLab and will be signed in. will be returned to GitLab and will be signed in.
## External Groups ## Marking Users as External based on SAML Groups
>**Note:** >**Note:**
This setting is only available on GitLab 8.7 and above. This setting is only available on GitLab 8.7 and above.
SAML login includes support for external groups. You can define in the SAML SAML login includes support for automatically identifying whether a user should
settings which groups, to which your users belong in your IdP, you wish to be be considered an [external](../user/permissions.md) user based on the user's group
marked as [external](../user/permissions.md). membership in the SAML identity provider. This feature **does not** allow you to
automatically add users to GitLab [Groups](../user/group/index.md), it simply
allows you to mark users as External if they are members of certain groups in the
Identity Provider.
### Requirements ### Requirements
......
...@@ -34,7 +34,6 @@ With **[GitLab Enterprise Edition][ee]**, you can also: ...@@ -34,7 +34,6 @@ With **[GitLab Enterprise Edition][ee]**, you can also:
- View the deployment process across projects with [Multi-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html#multi-project-pipeline-graphs) (available only in GitLab Enterprise Edition Premium) - View the deployment process across projects with [Multi-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html#multi-project-pipeline-graphs) (available only in GitLab Enterprise Edition Premium)
- Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers (available in GitLab Enterprise Edition Starter) - Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers (available in GitLab Enterprise Edition Starter)
- [Squash and merge](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html) for a cleaner commit history (available in GitLab Enterprise Edition Starter) - [Squash and merge](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html) for a cleaner commit history (available in GitLab Enterprise Edition Starter)
- Enable [semi-linear history merge requests](https://docs.gitlab.com/ee/user/project/merge_requests/index.html#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch (available in GitLab Enterprise Edition Starter)
- Analise the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) (available in GitLab Enterprise Edition Starter) - Analise the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) (available in GitLab Enterprise Edition Starter)
## Use cases ## Use cases
......
...@@ -313,13 +313,13 @@ module API ...@@ -313,13 +313,13 @@ module API
desc: 'The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., https://jira-api.example.com' desc: 'The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., https://jira-api.example.com'
}, },
{ {
required: false, required: true,
name: :username, name: :username,
type: String, type: String,
desc: 'The username of the user created to be used with GitLab/JIRA' desc: 'The username of the user created to be used with GitLab/JIRA'
}, },
{ {
required: false, required: true,
name: :password, name: :password,
type: String, type: String,
desc: 'The password of the user created to be used with GitLab/JIRA' desc: 'The password of the user created to be used with GitLab/JIRA'
......
...@@ -3,6 +3,14 @@ ...@@ -3,6 +3,14 @@
module Gitlab module Gitlab
module Git module Git
class Branch < Ref class Branch < Ref
def self.find(repo, branch_name)
if branch_name.is_a?(Gitlab::Git::Branch)
branch_name
else
repo.find_branch(branch_name)
end
end
def initialize(repository, name, target, target_commit) def initialize(repository, name, target, target_commit)
super(repository, name, target, target_commit) super(repository, name, target, target_commit)
end end
......
...@@ -72,7 +72,8 @@ module Gitlab ...@@ -72,7 +72,8 @@ module Gitlab
decorate(repo, commit) if commit decorate(repo, commit) if commit
rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError, rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError,
Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository,
Rugged::OdbError, Rugged::TreeError
nil nil
end end
......
...@@ -8,7 +8,7 @@ module Gitlab ...@@ -8,7 +8,7 @@ module Gitlab
def execute(pusher, repository, oldrev, newrev, ref) def execute(pusher, repository, oldrev, newrev, ref)
@repository = repository @repository = repository
@gl_id = pusher.gl_id @gl_id = pusher.gl_id
@gl_username = pusher.name @gl_username = pusher.username
@oldrev = oldrev @oldrev = oldrev
@newrev = newrev @newrev = newrev
@ref = ref @ref = ref
......
...@@ -166,7 +166,7 @@ module Gitlab ...@@ -166,7 +166,7 @@ module Gitlab
end end
def local_branches(sort_by: nil) def local_branches(sort_by: nil)
gitaly_migrate(:local_branches) do |is_enabled| gitaly_migrate(:local_branches, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled if is_enabled
gitaly_ref_client.local_branches(sort_by: sort_by) gitaly_ref_client.local_branches(sort_by: sort_by)
else else
...@@ -511,6 +511,10 @@ module Gitlab ...@@ -511,6 +511,10 @@ module Gitlab
gitaly_commit_client.ancestor?(from, to) gitaly_commit_client.ancestor?(from, to)
end end
def merged_branch_names(branch_names = [])
Set.new(git_merged_branch_names(branch_names))
end
# Return an array of Diff objects that represent the diff # Return an array of Diff objects that represent the diff
# between +from+ and +to+. See Diff::filter_diff_options for the allowed # between +from+ and +to+. See Diff::filter_diff_options for the allowed
# diff options. The +options+ hash can also include :break_rewrites to # diff options. The +options+ hash can also include :break_rewrites to
...@@ -745,6 +749,16 @@ module Gitlab ...@@ -745,6 +749,16 @@ module Gitlab
nil nil
end end
def ff_merge(user, source_sha, target_branch)
OperationService.new(user, self).with_branch(target_branch) do |our_commit|
raise ArgumentError, 'Invalid merge target' unless our_commit
source_sha
end
rescue Rugged::ReferenceError
raise ArgumentError, 'Invalid merge source'
end
def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:) def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
OperationService.new(user, self).with_branch( OperationService.new(user, self).with_branch(
branch_name, branch_name,
...@@ -1180,6 +1194,13 @@ module Gitlab ...@@ -1180,6 +1194,13 @@ module Gitlab
sort_branches(branches, sort_by) sort_branches(branches, sort_by)
end end
def git_merged_branch_names(branch_names = [])
lines = run_git(['branch', '--merged', root_ref] + branch_names)
.first.lines
lines.map(&:strip)
end
def log_using_shell?(options) def log_using_shell?(options)
options[:path].present? || options[:path].present? ||
options[:disable_walk] || options[:disable_walk] ||
......
...@@ -8,6 +8,7 @@ module Gitlab ...@@ -8,6 +8,7 @@ module Gitlab
{ name: name, email: email, message: message } { name: name, email: email, message: message }
end end
end end
PageBlob = Struct.new(:name)
def self.default_ref def self.default_ref
'master' 'master'
...@@ -80,7 +81,15 @@ module Gitlab ...@@ -80,7 +81,15 @@ module Gitlab
end end
def preview_slug(title, format) def preview_slug(title, format)
gollum_wiki.preview_page(title, '', format).url_path # Adapted from gollum gem (Gollum::Wiki#preview_page) to avoid
# using Rugged through a Gollum::Wiki instance
page_class = Gollum::Page
page = page_class.new(nil)
ext = page_class.format_to_ext(format.to_sym)
name = page_class.cname(title) + '.' + ext
blob = PageBlob.new(name)
page.populate(blob)
page.url_path
end end
private private
......
module Gitlab module Gitlab
module Git module Git
class WikiFile class WikiFile
attr_reader :mime_type, :raw_data, :name attr_reader :mime_type, :raw_data, :name, :path
# This class is meant to be serializable so that it can be constructed # This class is meant to be serializable so that it can be constructed
# by Gitaly and sent over the network to GitLab. # by Gitaly and sent over the network to GitLab.
...@@ -13,6 +13,7 @@ module Gitlab ...@@ -13,6 +13,7 @@ module Gitlab
@mime_type = gollum_file.mime_type @mime_type = gollum_file.mime_type
@raw_data = gollum_file.raw_data @raw_data = gollum_file.raw_data
@name = gollum_file.name @name = gollum_file.name
@path = gollum_file.path
end end
end end
end end
......
...@@ -215,10 +215,6 @@ module Gitlab ...@@ -215,10 +215,6 @@ module Gitlab
).exec ).exec
end end
def matching_merge_request?(newrev, branch_name)
Checks::MatchingMergeRequest.new(newrev, branch_name, project).match?
end
def deploy_key def deploy_key
actor if deploy_key? actor if deploy_key?
end end
......
...@@ -36,6 +36,10 @@ module OmniAuth ...@@ -36,6 +36,10 @@ module OmniAuth
email_response = access_token.get('api/2.0/user/emails').parsed email_response = access_token.get('api/2.0/user/emails').parsed
@emails ||= email_response && email_response['values'] || nil @emails ||= email_response && email_response['values'] || nil
end end
def callback_url
options[:redirect_uri] || (full_host + script_name + callback_path)
end
end end
end end
end end
...@@ -557,6 +557,29 @@ describe Projects::IssuesController do ...@@ -557,6 +557,29 @@ describe Projects::IssuesController do
end end
end end
end end
describe 'GET #edit' do
it_behaves_like 'restricted action', success: 200
def go(id:)
get :edit,
namespace_id: project.namespace.to_param,
project_id: project,
id: id
end
end
describe 'PUT #update' do
it_behaves_like 'restricted action', success: 302
def go(id:)
put :update,
namespace_id: project.namespace.to_param,
project_id: project,
id: id,
issue: { title: 'New title' }
end
end
end end
describe 'POST #create' do describe 'POST #create' do
......
...@@ -46,7 +46,7 @@ describe Projects::PipelinesController do ...@@ -46,7 +46,7 @@ describe Projects::PipelinesController do
context 'when performing gitaly calls', :request_store do context 'when performing gitaly calls', :request_store do
it 'limits the Gitaly requests' do it 'limits the Gitaly requests' do
expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(10) expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(8)
end end
end end
end end
......
...@@ -119,7 +119,7 @@ FactoryGirl.define do ...@@ -119,7 +119,7 @@ FactoryGirl.define do
finished_at nil finished_at nil
end end
factory :ci_build_tag do trait :tag do
tag true tag true
end end
......
FactoryGirl.define do
factory :instance_configuration do
skip_create
end
end
...@@ -38,6 +38,8 @@ FactoryGirl.define do ...@@ -38,6 +38,8 @@ FactoryGirl.define do
active true active true
properties( properties(
url: 'https://jira.example.com', url: 'https://jira.example.com',
username: 'jira_user',
password: 'my-secret-password',
project_key: 'jira-key' project_key: 'jira-key'
) )
end end
......
...@@ -218,15 +218,54 @@ describe 'New/edit issue', :js do ...@@ -218,15 +218,54 @@ describe 'New/edit issue', :js do
context 'edit issue' do context 'edit issue' do
before do before do
visit project_issue_path(project, issue) visit edit_project_issue_path(project, issue)
page.within('.content .issuable-actions') do end
click_on 'Edit'
it 'allows user to update issue' do
expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user.id.to_s)
expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
page.within '.js-user-search' do
expect(page).to have_content user.name
end
page.within '.js-milestone-select' do
expect(page).to have_content milestone.title
end
click_button 'Labels'
page.within '.dropdown-menu-labels' do
click_link label.title
click_link label2.title
end
page.within '.js-label-select' do
expect(page).to have_content label.title
end
expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
click_button 'Save changes'
page.within '.issuable-sidebar' do
page.within '.assignee' do
expect(page).to have_content user.name
end
page.within '.milestone' do
expect(page).to have_content milestone.title
end
page.within '.labels' do
expect(page).to have_content label.title
expect(page).to have_content label2.title
end
end end
end end
it 'description has autocomplete' do it 'description has autocomplete' do
find_field('issue-description').native.send_keys('') find('#issue_description').native.send_keys('')
fill_in 'issue-description', with: '@' fill_in 'issue_description', with: '@'
expect(page).to have_selector('.atwho-view') expect(page).to have_selector('.atwho-view')
end end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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