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._
- [Workflow labels](#workflow-labels)
- [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)
- [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)
- [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 triaging](#issue-triaging)
- [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';
window.Autosave = (function() {
function Autosave(field, key, resource) {
export default class Autosave {
constructor(field, key, resource) {
this.field = field;
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.resource = resource;
......@@ -12,14 +13,10 @@ window.Autosave = (function() {
this.key = 'autosave/' + key;
this.field.data('autosave', this);
this.restore();
this.field.on('input', (function(_this) {
return function() {
return _this.save();
};
})(this));
this.field.on('input', () => this.save());
}
Autosave.prototype.restore = function() {
restore() {
var text;
if (!this.isLocalStorageAvailable) return;
......@@ -40,9 +37,9 @@ window.Autosave = (function() {
field.dispatchEvent(event);
}
}
};
}
Autosave.prototype.save = function() {
save() {
var text;
text = this.field.val();
......@@ -51,15 +48,11 @@ window.Autosave = (function() {
}
return this.reset();
};
}
Autosave.prototype.reset = function() {
reset() {
if (!this.isLocalStorageAvailable) return;
return window.localStorage.removeItem(this.key);
};
return Autosave;
})();
export default window.Autosave;
}
}
......@@ -11,7 +11,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
// Issue boards is slightly different, we handle all the requests async
// instead or reloading the page, we just re-fire the list ajax requests
this.isHandledAsync = true;
this.cantEdit = cantEdit;
this.cantEdit = cantEdit.filter(i => typeof i === 'string');
this.cantEditWithValue = cantEdit.filter(i => typeof i === 'object');
}
updateObject(path) {
......@@ -42,7 +43,9 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
this.filteredSearchInput.dispatchEvent(new Event('input'));
}
canEdit(tokenName) {
return this.cantEdit.indexOf(tokenName) === -1;
canEdit(tokenName, tokenValue) {
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 = {
},
state: {},
detail: {
issue: {}
issue: {},
},
moving: {
issue: {},
list: {}
list: {},
},
create () {
this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&');
this.detail = { issue: {} };
this.detail = {
issue: {},
};
},
addList (listObj, defaultAvatar) {
const list = new List(listObj, defaultAvatar);
......
......@@ -13,7 +13,7 @@ import GroupLabelSubscription from './group_label_subscription';
/* global LineHighlighter */
import BuildArtifacts from './build_artifacts';
import CILintEditor from './ci_lint_editor';
/* global GroupsSelect */
import groupsSelect from './groups_select';
/* global Search */
/* global Admin */
/* global NamespaceSelects */
......@@ -414,7 +414,7 @@ import Diff from './diff';
break;
case 'projects:project_members:index':
memberExpirationDate('.js-access-expiration-date-groups');
new GroupsSelect();
groupsSelect();
memberExpirationDate();
new Members();
new UsersSelect();
......
......@@ -147,6 +147,16 @@ class DropdownUtils {
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)
static getSearchQuery(untilInput = false) {
const container = FilteredSearchContainer.container;
......
......@@ -185,8 +185,8 @@ class FilteredSearchManager {
if (e.keyCode === 8 || e.keyCode === 46) {
const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const sanitizedTokenName = lastVisualToken && lastVisualToken.querySelector('.name').textContent.trim();
const canEdit = sanitizedTokenName && this.canEdit && this.canEdit(sanitizedTokenName);
const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(lastVisualToken);
const canEdit = tokenName && this.canEdit && this.canEdit(tokenName, tokenValue);
if (this.filteredSearchInput.value === '' && lastVisualToken && canEdit) {
this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
gl.FilteredSearchVisualTokens.removeLastTokenPartial();
......@@ -336,8 +336,8 @@ class FilteredSearchManager {
let canClearToken = t.classList.contains('js-visual-token');
if (canClearToken) {
const tokenKey = t.querySelector('.name').textContent.trim();
canClearToken = this.canEdit && this.canEdit(tokenKey);
const { tokenName, tokenValue } = gl.DropdownUtils.getVisualTokenValues(t);
canClearToken = this.canEdit && this.canEdit(tokenName, tokenValue);
}
if (canClearToken) {
......@@ -469,7 +469,7 @@ class FilteredSearchManager {
}
hasFilteredSearch = true;
const canEdit = this.canEdit && this.canEdit(sanitizedKey);
const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue);
gl.FilteredSearchVisualTokens.addFilterVisualToken(
sanitizedKey,
`${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`,
......
......@@ -38,21 +38,14 @@ class FilteredSearchVisualTokens {
}
static createVisualTokenElementHTML(canEdit = true) {
let removeTokenMarkup = '';
if (canEdit) {
removeTokenMarkup = `
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</div>
`;
}
return `
<div class="selectable" role="button">
<div class="${canEdit ? 'selectable' : 'hidden'}" role="button">
<div class="name"></div>
<div class="value-container">
<div class="value"></div>
${removeTokenMarkup}
<div class="remove-token" role="button">
<i class="fa fa-close"></i>
</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 { 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() {
function GroupsSelect() {
$('.ajax-groups-select').each((function(_this) {
const self = _this;
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);
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,
page,
more,
pagination: {
more,
},
};
},
},
initSelection: function(element, callback) {
var id;
id = $(element).val();
if (id !== "") {
return Api.group(id, callback);
}
},
formatResult: function() {
var args;
args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
return self.formatResult.apply(self, args);
},
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;
};
})
.then(params.success)
.fail(params.error);
},
data(search, page) {
return {
search,
page,
per_page: window.GROUP_SELECT_PER_PAGE,
all_available: allAvailable,
};
},
results(data, page) {
if (data.length) return { results: [] };
GroupsSelect.prototype.forceOverflow = function (e) {
this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight)}px`;
};
const groups = data.length ? data : data.results || [];
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 */
/* global GitLab */
/* global Autosave */
import Pikaday from 'pikaday';
import Autosave from './autosave';
import UsersSelect from './users_select';
import GfmAutoComplete from './gfm_auto_complete';
import ZenMode from './zen_mode';
......
......@@ -8,7 +8,7 @@ import CreateLabelDropdown from './create_label';
(function() {
this.LabelsSelect = (function() {
function LabelsSelect(els) {
function LabelsSelect(els, options = {}) {
var _this, $els;
_this = this;
......@@ -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> <% }); %>');
labelNoneHTMLTemplate = '<span class="no-value">None</span>';
}
const handleClick = options.handleClick;
$sidebarLabelTooltip.tooltip();
......@@ -316,9 +317,9 @@ import CreateLabelDropdown from './create_label';
},
multiSelect: $dropdown.hasClass('js-multiselect'),
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(options) {
const { $el, e, isMarking } = options;
const label = options.selectedObj;
clicked: function(clickEvent) {
const { $el, e, isMarking } = clickEvent;
const label = clickEvent.selectedObj;
var isIssueIndex, isMRIndex, page, boardsModel;
var fadeOutLoader = () => {
......@@ -391,6 +392,10 @@ import CreateLabelDropdown from './create_label';
.then(fadeOutLoader)
.catch(fadeOutLoader);
}
else if (handleClick) {
e.preventDefault();
handleClick(label);
}
else {
if ($dropdown.hasClass('js-multiselect')) {
......
......@@ -41,7 +41,6 @@ import './behaviors/';
import './activities';
import './admin';
import './aside';
import './autosave';
import loadAwardsHandler from './awards_handler';
import bp from './breakpoints';
import './commits';
......@@ -55,7 +54,6 @@ import './gl_dropdown';
import './gl_field_error';
import './gl_field_errors';
import './gl_form';
import './groups_select';
import './header';
import './importer_status';
import './issuable_index';
......
......@@ -5,7 +5,7 @@ import _ from 'underscore';
(function() {
this.MilestoneSelect = (function() {
function MilestoneSelect(currentProject, els) {
function MilestoneSelect(currentProject, els, options = {}) {
var _this, $els;
if (currentProject != null) {
_this = this;
......@@ -136,19 +136,26 @@ import _ from 'underscore';
},
opened: function(e) {
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;
}
$('a.is-active', $el).removeClass('is-active');
$(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(options) {
const { $el, e } = options;
let selected = options.selectedObj;
clicked: function(clickEvent) {
const { $el, e } = clickEvent;
let selected = clickEvent.selectedObj;
var data, isIssueIndex, isMRIndex, isSelecting, page, boardsStore;
if (!selected) return;
if (options.handleClick) {
e.preventDefault();
options.handleClick(selected);
return;
}
page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
......
......@@ -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,
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 */
/* global Autosave */
/* global ResolveService */
/* global mrRefreshWidgetUrl */
......@@ -20,7 +20,7 @@ import Flash from './flash';
import CommentTypeToggle from './comment_type_toggle';
import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler';
import './autosave';
import Autosave from './autosave';
import TaskList from './task_list';
import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
......
<script>
/* global Autosave */
import { mapActions, mapGetters } from 'vuex';
import _ from 'underscore';
import autosize from 'vendor/autosize';
import Flash from '../../flash';
import '../../autosave';
import Autosave from '../../autosave';
import TaskList from '../../task_list';
import * as constants from '../constants';
import eventHub from '../event_hub';
......
......@@ -9,8 +9,8 @@
import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue';
import issueNoteEditedText from './issue_note_edited_text.vue';
import issueNoteForm from './issue_note_form.vue';
import placeholderNote from './issue_placeholder_note.vue';
import placeholderSystemNote from './issue_placeholder_system_note.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import autosave from '../mixins/autosave';
export default {
......
......@@ -5,10 +5,10 @@
import * as constants from '../constants';
import issueNote from './issue_note.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 placeholderNote from './issue_placeholder_note.vue';
import placeholderSystemNote from './issue_placeholder_system_note.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
......@@ -37,7 +37,7 @@
components: {
issueNote,
issueDiscussion,
issueSystemNote,
systemNote,
issueCommentForm,
loadingIcon,
placeholderNote,
......@@ -68,7 +68,7 @@
}
return placeholderNote;
} else if (note.individual_note) {
return note.notes[0].system ? issueSystemNote : issueNote;
return note.notes[0].system ? systemNote : issueNote;
}
return issueDiscussion;
......
/* globals Autosave */
import '../../autosave';
import Autosave from '../../autosave';
export default {
methods: {
......
......@@ -6,7 +6,7 @@ import _ from 'underscore';
// TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
function UsersSelect(currentUser, els) {
function UsersSelect(currentUser, els, options = {}) {
var $els;
this.users = this.users.bind(this);
this.user = this.user.bind(this);
......@@ -20,6 +20,8 @@ function UsersSelect(currentUser, els) {
}
}
const { handleClick } = options;
$els = $(els);
if (!els) {
......@@ -442,6 +444,9 @@ function UsersSelect(currentUser, els) {
}
if ($el.closest('.add-issues-modal').length) {
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)) {
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
......
......@@ -286,6 +286,7 @@ export default {
<input
id="remove-source-branch-input"
v-model="removeSourceBranch"
class="js-remove-source-branch-checkbox"
:disabled="isRemoveSourceBranchButtonDisabled"
type="checkbox"/> Remove source branch
</label>
......@@ -311,8 +312,8 @@ export default {
</button>
</template>
<template v-else>
<span class="bold">
The pipeline for this merge request has not succeeded yet
<span class="bold js-resolve-mr-widget-items-message">
You can only merge once the items above are resolved
</span>
</template>
</div>
......
......@@ -18,6 +18,12 @@
required: false,
default: false,
},
class: {
type: String,
required: false,
default: '',
},
},
computed: {
......@@ -25,7 +31,7 @@
return this.inline ? 'span' : 'div';
},
cssClass() {
return `fa-${this.size}x`;
return `fa-${this.size}x ${this.class}`.trim();
},
},
};
......
<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 userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import userAvatarLink from '../user_avatar/user_avatar_link.vue';
export default {
name: 'issuePlaceholderNote',
name: 'placeholderNote',
props: {
note: {
type: Object,
......
<script>
/**
* Common component to render a placeholder system note.
*
* @example
* <placeholder-system-note
* :note="{ body: 'Commands are being applied'}"
* />
*/
export default {
name: 'placeholderSystemNote',
props: {
......
<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 issueNoteHeader from './issue_note_header.vue';
import issueNoteHeader from '../../../notes/components/issue_note_header.vue';
import { spriteIcon } from '../../../lib/utils/common_utils';
export default {
name: 'systemNote',
......@@ -24,7 +42,7 @@
return this.targetNoteHash === this.noteAnchorId;
},
iconHtml() {
return gl.utils.spriteIcon(this.note.system_note_icon_name);
return spriteIcon(this.note.system_note_icon_name);
},
},
};
......@@ -46,7 +64,8 @@
:author="note.author"
:created-at="note.created_at"
:note-id="note.id"
:action-text-html="note.note_html" />
:action-text-html="note.note_html"
/>
</div>
</div>
</div>
......
......@@ -5,17 +5,27 @@ export default {
props: {
title: {
type: String,
required: true,
required: false,
},
text: {
type: String,
required: false,
},
hideFooter: {
type: Boolean,
required: false,
default: false,
},
kind: {
type: String,
required: false,
default: 'primary',
},
modalDialogClass: {
type: String,
required: false,
default: '',
},
closeKind: {
type: String,
required: false,
......@@ -30,6 +40,11 @@ export default {
type: String,
required: true,
},
submitDisabled: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
......@@ -57,43 +72,57 @@ export default {
</script>
<template>
<div
class="modal popup-dialog"
role="dialog"
tabindex="-1">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button"
class="close"
@click="close"
aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">{{this.title}}</h4>
</div>
<div class="modal-body">
<slot name="body" :text="text">
<p>{{text}}</p>
</slot>
</div>
<div class="modal-footer">
<button
type="button"
class="btn"
:class="btnCancelKindClass"
@click="close">
{{ closeButtonLabel }}
</button>
<button
type="button"
class="btn"
:class="btnKindClass"
@click="emitSubmit(true)">
{{ primaryButtonLabel }}
</button>
<div class="modal-open">
<div
class="modal popup-dialog"
role="dialog"
tabindex="-1"
>
<div
:class="modalDialogClass"
class="modal-dialog"
role="document"
>
<div class="modal-content">
<div class="modal-header">
<slot name="header">
<h4 class="modal-title pull-left">
{{this.title}}
</h4>
<button
type="button"
class="close pull-right"
@click="close"
aria-label="Close"
>
<span aria-hidden="true">&times;</span>
</button>
</slot>
</div>
<div class="modal-body">
<slot name="body" :text="text">
<p>{{this.text}}</p>
</slot>
</div>
<div class="modal-footer" v-if="!hideFooter">
<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 class="modal-backdrop fade in" />
</div>
</template>
......@@ -12,12 +12,14 @@
:img-alt="tooltipText"
:img-size="20"
:tooltip-text="tooltipText"
tooltip-placement="top"
:tooltip-placement="top"
:username="username"
/>
*/
import userAvatarImage from './user_avatar_image.vue';
import tooltip from '../../directives/tooltip';
export default {
name: 'UserAvatarLink',
......@@ -60,6 +62,22 @@ export default {
required: false,
default: 'top',
},
username: {
type: String,
required: false,
default: '',
},
},
computed: {
shouldShowUsername() {
return this.username.length > 0;
},
avatarTooltipText() {
return this.shouldShowUsername ? '' : this.tooltipText;
},
},
directives: {
tooltip,
},
};
</script>
......@@ -73,8 +91,13 @@ export default {
:img-alt="imgAlt"
:css-classes="imgCssClasses"
:size="imgSize"
:tooltip-text="tooltipText"
:tooltip-text="avatarTooltipText"
:tooltip-placement="tooltipPlacement"
/><span
v-if="shouldShowUsername"
v-tooltip
:title="tooltipText"
:tooltip-placement="tooltipPlacement"
/>
>{{username}}</span>
</a>
</template>
......@@ -4,6 +4,9 @@
.cred { color: $common-red; }
.cgreen { color: $common-green; }
.cdark { color: $common-gray-dark; }
.text-secondary {
color: $gl-text-color-secondary;
}
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
......
......@@ -37,6 +37,7 @@
.dropdown-menu-nav {
@include set-visible;
display: block;
min-height: 40px;
@media (max-width: $screen-xs-max) {
width: 100%;
......
......@@ -65,7 +65,7 @@
display: flex;
flex: 1;
-webkit-flex: 1;
padding-left: 30px;
padding-left: 12px;
position: relative;
margin-bottom: 0;
}
......@@ -221,10 +221,6 @@
box-shadow: 0 0 4px $search-input-focus-shadow-color;
}
&.focus .fa-filter {
color: $common-gray-dark;
}
gl-emoji {
display: inline-block;
font-family: inherit;
......@@ -251,13 +247,6 @@
}
}
.fa-filter {
position: absolute;
top: 10px;
left: 10px;
color: $gray-darkest;
}
.fa-times {
right: 10px;
color: $gray-darkest;
......
......@@ -42,3 +42,11 @@ body.modal-open {
width: 98%;
}
}
.modal.popup-dialog {
display: block;
}
.modal-body {
background-color: $modal-body-bg;
}
......@@ -164,3 +164,36 @@ $pre-border-color: $border-color;
$table-bg-accent: $gray-light;
$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-target-form {
display: inline-block;
......
......@@ -10,7 +10,7 @@ class ConfirmationsController < Devise::ConfirmationsController
users_almost_there_path
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
if signed_in?(:user)
after_sign_in(resource)
......
......@@ -57,6 +57,10 @@ class HelpController < ApplicationController
def shortcuts
end
def instance_configuration
@instance_configuration = InstanceConfiguration.new
end
def ui
@user = User.new(id: 0, name: 'John Doe', username: '@johndoe')
end
......
......@@ -15,6 +15,8 @@ class Projects::BranchesController < Projects::ApplicationController
respond_to do |format|
format.html do
@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
Gitlab::GitalyClient.allow_n_plus_1_calls do
@max_commits = @branches.reduce(0) do |memo, branch|
......
......@@ -16,7 +16,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_create_issue!, only: [:new, :create]
# 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
before_action :authorize_create_merge_request!, only: [:create_merge_request]
......@@ -63,6 +63,10 @@ class Projects::IssuesController < Projects::ApplicationController
respond_with(@issue)
end
def edit
respond_with(@issue)
end
def show
@noteable = @issue
@note = @project.notes.new(noteable: @issue)
......@@ -122,6 +126,10 @@ class Projects::IssuesController < Projects::ApplicationController
@issue = Issues::UpdateService.new(project, current_user, update_params).execute(issue)
respond_to do |format|
format.html do
recaptcha_check_with_fallback { render :edit }
end
format.json do
render_issue_json
end
......
......@@ -23,7 +23,7 @@ class BranchesFinder
def filter_by_name(branches)
if search
branches.select { |branch| branch.name.include?(search) }
branches.select { |branch| branch.name.upcase.include?(search.upcase) }
else
branches
end
......
......@@ -20,17 +20,6 @@ module BoardsHelper
project_issues_path(@project)
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
project_boards_path(@project)
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
end
def commit
@commit ||= project.commit(sha)
rescue
nil
@commit ||= project.commit_by(oid: sha)
end
def branch?
......
......@@ -14,6 +14,8 @@ class Email < ActiveRecord::Base
devise :confirmable
self.reconfirmable = false # currently email can't be changed, no need to reconfirm
delegate :username, to: :user
def email=(value)
write_attribute(:email, value.downcase.strip)
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
end
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)
end
......
......@@ -540,6 +540,10 @@ class Project < ActiveRecord::Base
repository.commit(ref)
end
def commit_by(oid:)
repository.commit_by(oid: oid)
end
# ref can't be HEAD, can only be branch/tag name or SHA
def latest_successful_builds_for(ref = default_branch)
latest_pipeline = pipelines.latest_successful_for(ref)
......@@ -553,7 +557,7 @@ class Project < ActiveRecord::Base
def merge_base_commit(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
def saved?
......
......@@ -3,6 +3,8 @@ class JiraService < IssueTrackerService
validates :url, url: true, presence: true, if: :activated?
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
......
......@@ -153,7 +153,10 @@ class KubernetesService < DeploymentService
end
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
def build_kubeclient!(api_path: 'api', api_version: 'v1')
......
......@@ -76,6 +76,7 @@ class Repository
@full_path = full_path
@disk_path = disk_path || full_path
@project = project
@commit_cache = {}
end
def ==(other)
......@@ -103,18 +104,17 @@ class Repository
def commit(ref = 'HEAD')
return nil unless exists?
return ref if ref.is_a?(::Commit)
commit =
if ref.is_a?(Gitlab::Git::Commit)
ref
else
Gitlab::Git::Commit.find(raw_repository, ref)
end
find_commit(ref)
end
commit = ::Commit.new(commit, @project) if commit
commit
rescue Rugged::OdbError, Rugged::TreeError
nil
# Finding a commit by the passed SHA
# Also takes care of caching, based on the SHA
def commit_by(oid:)
return @commit_cache[oid] if @commit_cache.key?(oid)
@commit_cache[oid] = find_commit(oid)
end
def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil)
......@@ -231,7 +231,7 @@ class Repository
# branches or tags, but we want to keep some of these commits around, for
# example if they have comments or CI builds.
def keep_around(sha)
return unless sha && commit(sha)
return unless sha && commit_by(oid: sha)
return if kept_around?(sha)
......@@ -862,22 +862,12 @@ class Repository
end
def ff_merge(user, source, target_branch, merge_request: nil)
our_commit = rugged.branches[target_branch].target
their_commit =
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?
their_commit_id = commit(source)&.id
raise 'Invalid merge source' if their_commit_id.nil?
with_branch(user, target_branch) do |start_commit|
merge_request&.update(in_progress_merge_commit_sha: their_commit.oid)
merge_request&.update(in_progress_merge_commit_sha: their_commit_id)
their_commit.oid
end
with_cache_hooks { raw.ff_merge(user, their_commit_id, target_branch) }
end
def revert(
......@@ -912,18 +902,27 @@ class Repository
end
end
def merged_to_root_ref?(branch_name)
branch_commit = commit(branch_name)
root_ref_commit = commit(root_ref)
def merged_to_root_ref?(branch_or_name, pre_loaded_merged_branches = nil)
branch = Gitlab::Git::Branch.find(self, branch_or_name)
if branch_commit
same_head = branch_commit.id == root_ref_commit.id
!same_head && ancestor?(branch_commit.id, root_ref_commit.id)
if branch
root_ref_sha = commit(root_ref).sha
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
nil
end
end
delegate :merged_branch_names, to: :raw_repository
def merge_base(first_commit_id, second_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
......@@ -1031,6 +1030,10 @@ class Repository
if instance_variable_defined?(ivar)
instance_variable_get(ivar)
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
value =
if memoize_only
......@@ -1040,8 +1043,9 @@ class Repository
end
instance_variable_set(ivar, value)
rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository
# if e.g. HEAD or the entire repository doesn't exist we want to
# gracefully handle this and not cache anything.
# Even if the above `#exists?` check passes these errors might still
# occur (for example because of a non-existing HEAD). We want to
# gracefully handle this and not cache anything
fallback
end
end
......@@ -1069,6 +1073,18 @@ class Repository
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)
blob = blob_at(sha, path)
return unless blob
......@@ -1107,12 +1123,12 @@ class Repository
def last_commit_for_path_by_gitaly(sha, path)
c = raw_repository.gitaly_commit_client.last_commit_for_path(sha, path)
commit(c)
commit_by(oid: c)
end
def last_commit_for_path_by_rugged(sha, path)
sha = last_commit_id_for_path_by_shelling_out(sha, path)
commit(sha)
commit_by(oid: sha)
end
def last_commit_id_for_path_by_shelling_out(sha, path)
......
......@@ -82,16 +82,9 @@ module MergeRequests
@merge_request.can_remove_source_branch?(branch_deletion_user)
end
# Logs merge error message and cleans `MergeRequest#merge_jid`.
#
def handle_merge_error(log_message:, save_message_on_model: false)
Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{log_message}")
if save_message_on_model
@merge_request.update(merge_error: log_message, merge_jid: nil)
else
clean_merge_jid
end
@merge_request.update(merge_error: log_message) if save_message_on_model
end
def merge_request_info
......
......@@ -83,7 +83,7 @@ class SystemHooksService
project_id: model.id,
owner_name: owner.name,
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
......
......@@ -6,13 +6,13 @@
.fade-right= icon('angle-right')
%ul.nav-links.scrolling-tabs
= 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
= 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
= 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
.nav-controls
......
......@@ -11,6 +11,7 @@
%span= Gitlab::VERSION
%small= link_to Gitlab::REVISION, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab::REVISION)
= version_status_badge
%p.slead
GitLab is open source software to collaborate on code.
%br
......@@ -23,6 +24,7 @@
Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises.
%br
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
.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)
- bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0
- diverging_commit_counts = @repository.diverging_commit_counts(branch)
......@@ -12,7 +13,7 @@
&nbsp;
- if branch.name == @repository.root_ref
%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 } }
= s_('Branches|merged')
......@@ -47,7 +48,7 @@
target: "#modal-delete-branch",
delete_path: project_branch_path(@project, 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")
- else
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
......
......@@ -38,7 +38,7 @@
- if @branches.any?
%ul.content-list.all-branches
- @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'
- else
.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 @@
%ul.tokens-container.list-unstyled
%li.input-token
%input.form-control.filtered-search{ search_filter_input_options(type) }
= icon('filter')
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } }
......
......@@ -23,7 +23,7 @@ class StuckMergeJobsWorker
merge_requests = MergeRequest.where(id: completed_ids)
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(', ')}")
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/shortcuts' => 'help#shortcuts'
get 'help/ui' => 'help#ui'
get 'help/*path' => 'help#show', as: :help_page
get 'help' => 'help#index'
get 'help/shortcuts' => 'help#shortcuts'
get 'help/ui' => 'help#ui'
get 'help/instance_configuration' => 'help#instance_configuration'
get 'help/*path' => 'help#show', as: :help_page
......@@ -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`. |
| `project_key` | string | yes | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. |
| `username` | string | no | The username of the user created to be used with GitLab/JIRA. |
| `password` | string | no | The password of the user created to be used with GitLab/JIRA. |
| `username` | string | yes | The username 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`. |
### Delete JIRA service
......
# Guidelines for implementing Enterprise Edition feature
- **Write the code and the tests.**: As with any code, EE features should have
good test coverage to prevent regressions.
- **Write documentation.**: Add documentation to the `doc/` directory. Describe
the feature and include screenshots, if applicable.
- **Submit a MR to the `www-gitlab-com` project.**: Add the new feature to the
[EE features list][ee-features-list].
## Act as CE when unlicensed
Since the implementation of [GitLab CE features to work with unlicensed EE instance][ee-as-ce]
GitLab Enterprise Edition should work like GitLab Community Edition
when no license is active. So EE features always should be guarded by
`project.feature_available?` or `group.feature_available?` (or
`License.feature_available?` if it is a system-wide feature).
CE specs should remain untouched as much as possible and extra specs
should be added for EE. Licensed features can be stubbed using the
spec helper `stub_licensed_features` in `EE::LicenseHelpers`.
[ee-as-ce]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2500
## Separation of EE code
We want a [single code base][] eventually, but before we reach the goal,
we still need to merge changes from GitLab CE to EE. To help us get there,
we should make sure that we no longer edit CE files in place in order to
implement EE features.
Instead, all EE codes should be put inside the `ee/` top-level directory, and
tests should be put inside `spec/ee/`. We don't use `ee/spec` for now due to
technical limitation. The rest of codes should be as close as to the CE files.
[single code base]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2952#note_41016454
### EE-only features
If the feature being developed is not present in any form in CE, we don't
need to put the codes under `EE` namespace. For example, an EE model could
go into: `ee/app/models/awesome.rb` using `Awesome` as the class name. This
is applied not only to models. Here's a list of other examples:
- `ee/app/controllers/foos_controller.rb`
- `ee/app/finders/foos_finder.rb`
- `ee/app/helpers/foos_helper.rb`
- `ee/app/mailers/foos_mailer.rb`
- `ee/app/models/foo.rb`
- `ee/app/policies/foo_policy.rb`
- `ee/app/serializers/foo_entity.rb`
- `ee/app/serializers/foo_serializer.rb`
- `ee/app/services/foo/create_service.rb`
- `ee/app/validators/foo_attr_validator.rb`
- `ee/app/workers/foo_worker.rb`
### EE features based on CE features
For features that build on existing CE features, write a module in the
`EE` namespace and `prepend` it in the CE class. This makes conflicts
less likely to happen during CE to EE merges because only one line is
added to the CE class - the `prepend` line.
Since the module would require an `EE` namespace, the file should also be
put in an `ee/` sub-directory. For example, we want to extend the user model
in EE, so we have a module called `::EE::User` put inside
`ee/app/models/ee/user.rb`.
This is also not just applied to models. Here's a list of other examples:
- `ee/app/controllers/ee/foos_controller.rb`
- `ee/app/finders/ee/foos_finder.rb`
- `ee/app/helpers/ee/foos_helper.rb`
- `ee/app/mailers/ee/foos_mailer.rb`
- `ee/app/models/ee/foo.rb`
- `ee/app/policies/ee/foo_policy.rb`
- `ee/app/serializers/ee/foo_entity.rb`
- `ee/app/serializers/ee/foo_serializer.rb`
- `ee/app/services/ee/foo/create_service.rb`
- `ee/app/validators/ee/foo_attr_validator.rb`
- `ee/app/workers/ee/foo_worker.rb`
#### Overriding CE methods
To override a method present in the CE codebase, use `prepend`. It
lets you override a method in a class with a method from a module, while
still having access the class's implementation with `super`.
There are a few gotchas with it:
- you should always add a `raise NotImplementedError unless defined?(super)`
guard clause in the "overrider" method to ensure that if the method gets
renamed in CE, the EE override won't be silently forgotten.
- when the "overrider" would add a line in the middle of the CE
implementation, you should refactor the CE method and split it in
smaller methods. Or create a "hook" method that is empty in CE,
and with the EE-specific implementation in EE.
- when the original implementation contains a guard clause (e.g.
`return unless condition`), we cannot easily extend the behaviour by
overriding the method, because we can't know when the overridden method
(i.e. calling `super` in the overriding method) would want to stop early.
In this case, we shouldn't just override it, but update the original method
to make it call the other method we want to extend, like a [template method
pattern](https://en.wikipedia.org/wiki/Template_method_pattern).
For example, given this base:
``` ruby
class Base
def execute
return unless enabled?
# ...
# ...
end
end
```
Instead of just overriding `Base#execute`, we should update it and extract
the behaviour into another method:
``` ruby
class Base
def execute
return unless enabled?
do_something
end
private
def do_something
# ...
# ...
end
end
```
Then we're free to override that `do_something` without worrying about the
guards:
``` ruby
module EE::Base
def do_something
# Follow the above pattern to call super and extend it
end
end
```
This would require updating CE first, or make sure this is back ported to CE.
When prepending, place them in the `ee/` specific sub-directory, and
wrap class or module in `module EE` to avoid naming conflicts.
For example to override the CE implementation of
`ApplicationController#after_sign_out_path_for`:
```ruby
def after_sign_out_path_for(resource)
current_application_settings.after_sign_out_path.presence || new_user_session_path
end
```
Instead of modifying the method in place, you should add `prepend` to
the existing file:
```ruby
class ApplicationController < ActionController::Base
prepend EE::ApplicationController
# ...
def after_sign_out_path_for(resource)
current_application_settings.after_sign_out_path.presence || new_user_session_path
end
# ...
end
```
And create a new file in the `ee/` sub-directory with the altered
implementation:
```ruby
module EE
class ApplicationController
def after_sign_out_path_for(resource)
raise NotImplementedError unless defined?(super)
if Gitlab::Geo.secondary?
Gitlab::Geo.primary_node.oauth_logout_url(@geo_logout_state)
else
super
end
end
end
end
```
#### Use self-descriptive wrapper methods
When it's not possible/logical to modify the implementation of a
method. Wrap it in a self-descriptive method and use that method.
For example, in CE only an `admin` is allowed to access all private
projects/groups, but in EE also an `auditor` has full private
access. It would be incorrect to override the implementation of
`User#admin?`, so instead add a method `full_private_access?` to
`app/models/users.rb`. The implementation in CE will be:
```ruby
def full_private_access?
admin?
end
```
In EE, the implementation `ee/app/models/ee/users.rb` would be:
```ruby
def full_private_access?
raise NotImplementedError unless defined?(super)
super || auditor?
end
```
In `lib/gitlab/visibility_level.rb` this method is used to return the
allowed visibilty levels:
```ruby
def levels_for_user(user = nil)
if user.full_private_access?
[PRIVATE, INTERNAL, PUBLIC]
elsif # ...
end
```
See [CE MR][ce-mr-full-private] and [EE MR][ee-mr-full-private] for
full implementation details.
[ce-mr-full-private]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12373
[ee-mr-full-private]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2199
### Code in `app/controllers/`
In controllers, the most common type of conflict is with `before_action` that
has a list of actions in CE but EE adds some actions to that list.
The same problem often occurs for `params.require` / `params.permit` calls.
**Mitigations**
Separate CE and EE actions/keywords. For instance for `params.require` in
`ProjectsController`:
```ruby
def project_params
params.require(:project).permit(project_params_attributes)
end
# Always returns an array of symbols, created however best fits the use case.
# It _should_ be sorted alphabetically.
def project_params_attributes
%i[
description
name
path
]
end
```
In the `EE::ProjectsController` module:
```ruby
def project_params_attributes
super + project_params_attributes_ee
end
def project_params_attributes_ee
%i[
approvals_before_merge
approver_group_ids
approver_ids
...
]
end
```
### Code in `app/models/`
EE-specific models should `extend EE::Model`.
For example, if EE has a specific `Tanuki` model, you would
place it in `ee/app/models/ee/tanuki.rb`.
### Code in `app/views/`
It's a very frequent problem that EE is adding some specific view code in a CE
view. For instance the approval code in the project's settings page.
**Mitigations**
Blocks of code that are EE-specific should be moved to partials. This
avoids conflicts with big chunks of HAML code that that are not fun to
resolve when you add the indentation to the equation.
EE-specific views should be placed in `ee/app/views/ee/`, using extra
sub-directories if appropriate.
### Code in `lib/`
Place EE-specific logic in the top-level `EE` module namespace. Namespace the
class beneath the `EE` module just as you would normally.
For example, if CE has LDAP classes in `lib/gitlab/ldap/` then you would place
EE-specific LDAP classes in `ee/lib/ee/gitlab/ldap`.
### Code in `spec/`
When you're testing EE-only features, avoid adding examples to the
existing CE specs. Also do no change existing CE examples, since they
should remain working as-is when EE is running without a license.
Instead place EE specs in the `spec/ee/spec` folder.
## JavaScript code in `assets/javascripts/`
To separate EE-specific JS-files we can also move the files into an `ee` folder.
For example there can be an
`app/assets/javascripts/protected_branches/protected_branches_bundle.js` and an
EE counterpart
`ee/app/assets/javascripts/protected_branches/protected_branches_bundle.js`.
That way we can create a separate webpack bundle in `webpack.config.js`:
```javascript
protected_branches: '~/protected_branches',
ee_protected_branches: 'ee/protected_branches/protected_branches_bundle.js',
```
With the separate bundle in place, we can decide which bundle to load inside the
view, using the `page_specific_javascript_bundle_tag` helper.
```haml
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('protected_branches')
```
## SCSS code in `assets/stylesheets`
To separate EE-specific styles in SCSS files, if a component you're adding styles for
is limited to only EE, it is better to have a separate SCSS file in appropriate directory
within `app/assets/stylesheets`.
In some cases, this is not entirely possible or creating dedicated SCSS file is an overkill,
e.g. a text style of some component is different for EE. In such cases,
styles are usually kept in stylesheet that is common for both CE and EE, and it is wise
to isolate such ruleset from rest of CE rules (along with adding comment describing the same)
to avoid conflicts during CE to EE merge.
#### Bad
```scss
.section-body {
.section-title {
background: $gl-header-color;
}
&.ee-section-body {
.section-title {
background: $gl-header-color-cyan;
}
}
}
```
#### Good
```scss
.section-body {
.section-title {
background: $gl-header-color;
}
}
/* EE-specific styles */
.section-body.ee-section-body {
.section-title {
background: $gl-header-color-cyan;
}
}
```
......@@ -3,31 +3,28 @@
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
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)
and [pushed your changes](basic-git-commands.md) to GitLab.
1. You can then go to the project where you'd like to merge your changes and
click on the **Merge requests** tab.
![Merge requests](img/project_navbar.png)
1. Go to the project where you'd like to merge your changes and click on the
**Merge requests** tab.
1. Click on **New merge request** on the right side of the screen.
![New Merge Request](img/merge_request_new.png)
1. Select a source branch and click on the **Compare branches and continue** button.
1. From there on, you have the option to select the source branch and the target
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.
![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,
select a user to review your merge request and to accept or close it. You may
also select a milestone and labels.
![New merge request page](img/merge_request_page.png)
1. When ready, click on the **Submit merge request** button. Your merge request
will be ready to be approved and published.
1. When ready, click on the **Submit merge request** button.
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
Click the icon to begin the authentication process. If everything goes well the user
will be returned to GitLab and will be signed in.
## External Groups
## Marking Users as External based on SAML Groups
>**Note:**
This setting is only available on GitLab 8.7 and above.
SAML login includes support for external groups. You can define in the SAML
settings which groups, to which your users belong in your IdP, you wish to be
marked as [external](../user/permissions.md).
SAML login includes support for automatically identifying whether a user should
be considered an [external](../user/permissions.md) user based on the user's group
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
......
......@@ -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)
- 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)
- 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)
## Use cases
......
......@@ -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'
},
{
required: false,
required: true,
name: :username,
type: String,
desc: 'The username of the user created to be used with GitLab/JIRA'
},
{
required: false,
required: true,
name: :password,
type: String,
desc: 'The password of the user created to be used with GitLab/JIRA'
......
......@@ -3,6 +3,14 @@
module Gitlab
module Git
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)
super(repository, name, target, target_commit)
end
......
......@@ -72,7 +72,8 @@ module Gitlab
decorate(repo, commit) if commit
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
end
......
......@@ -8,7 +8,7 @@ module Gitlab
def execute(pusher, repository, oldrev, newrev, ref)
@repository = repository
@gl_id = pusher.gl_id
@gl_username = pusher.name
@gl_username = pusher.username
@oldrev = oldrev
@newrev = newrev
@ref = ref
......
......@@ -166,7 +166,7 @@ module Gitlab
end
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
gitaly_ref_client.local_branches(sort_by: sort_by)
else
......@@ -511,6 +511,10 @@ module Gitlab
gitaly_commit_client.ancestor?(from, to)
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
# between +from+ and +to+. See Diff::filter_diff_options for the allowed
# diff options. The +options+ hash can also include :break_rewrites to
......@@ -745,6 +749,16 @@ module Gitlab
nil
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:)
OperationService.new(user, self).with_branch(
branch_name,
......@@ -1180,6 +1194,13 @@ module Gitlab
sort_branches(branches, sort_by)
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)
options[:path].present? ||
options[:disable_walk] ||
......
......@@ -8,6 +8,7 @@ module Gitlab
{ name: name, email: email, message: message }
end
end
PageBlob = Struct.new(:name)
def self.default_ref
'master'
......@@ -80,7 +81,15 @@ module Gitlab
end
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
private
......
module Gitlab
module Git
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
# by Gitaly and sent over the network to GitLab.
......@@ -13,6 +13,7 @@ module Gitlab
@mime_type = gollum_file.mime_type
@raw_data = gollum_file.raw_data
@name = gollum_file.name
@path = gollum_file.path
end
end
end
......
......@@ -215,10 +215,6 @@ module Gitlab
).exec
end
def matching_merge_request?(newrev, branch_name)
Checks::MatchingMergeRequest.new(newrev, branch_name, project).match?
end
def deploy_key
actor if deploy_key?
end
......
......@@ -36,6 +36,10 @@ module OmniAuth
email_response = access_token.get('api/2.0/user/emails').parsed
@emails ||= email_response && email_response['values'] || nil
end
def callback_url
options[:redirect_uri] || (full_host + script_name + callback_path)
end
end
end
end
......@@ -557,6 +557,29 @@ describe Projects::IssuesController do
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
describe 'POST #create' do
......
......@@ -46,7 +46,7 @@ describe Projects::PipelinesController do
context 'when performing gitaly calls', :request_store 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
......
......@@ -119,7 +119,7 @@ FactoryGirl.define do
finished_at nil
end
factory :ci_build_tag do
trait :tag do
tag true
end
......
FactoryGirl.define do
factory :instance_configuration do
skip_create
end
end
......@@ -38,6 +38,8 @@ FactoryGirl.define do
active true
properties(
url: 'https://jira.example.com',
username: 'jira_user',
password: 'my-secret-password',
project_key: 'jira-key'
)
end
......
......@@ -218,15 +218,54 @@ describe 'New/edit issue', :js do
context 'edit issue' do
before do
visit project_issue_path(project, issue)
page.within('.content .issuable-actions') do
click_on 'Edit'
visit edit_project_issue_path(project, issue)
end
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
it 'description has autocomplete' do
find_field('issue-description').native.send_keys('')
fill_in 'issue-description', with: '@'
find('#issue_description').native.send_keys('')
fill_in 'issue_description', with: '@'
expect(page).to have_selector('.atwho-view')
end
......
require 'spec_helper'
describe 'Issues', :js do
describe 'Issues' do
include DropzoneHelper
include IssueHelpers
include SortingHelper
......@@ -24,15 +24,109 @@ describe 'Issues', :js do
end
before do
visit project_issue_path(project, issue)
page.within('.content .issuable-actions') do
find('.issuable-edit').click
end
find('.issue-details .content-block .js-zen-enter').click
visit edit_project_issue_path(project, issue)
find('.js-zen-enter').click
end
it 'opens new issue popup' do
expect(page).to have_content(issue.description)
expect(page).to have_content("Issue ##{issue.iid}")
end
end
describe 'Editing issue assignee' do
let!(:issue) do
create(:issue,
author: user,
assignees: [user],
project: project)
end
it 'allows user to select unassigned', :js do
visit edit_project_issue_path(project, issue)
expect(page).to have_content "Assignee #{user.name}"
first('.js-user-search').click
click_link 'Unassigned'
click_button 'Save changes'
page.within('.assignee') do
expect(page).to have_content 'No assignee - assign yourself'
end
expect(issue.reload.assignees).to be_empty
end
end
describe 'due date', :js do
context 'on new form' do
before do
visit new_project_issue_path(project)
end
it 'saves with due date' do
date = Date.today.at_beginning_of_month
fill_in 'issue_title', with: 'bug 345'
fill_in 'issue_description', with: 'bug description'
find('#issuable-due-date').click
page.within '.pika-single' do
click_button date.day
end
expect(find('#issuable-due-date').value).to eq date.to_s
click_button 'Submit issue'
page.within '.issuable-sidebar' do
expect(page).to have_content date.to_s(:medium)
end
end
end
context 'on edit form' do
let(:issue) { create(:issue, author: user, project: project, due_date: Date.today.at_beginning_of_month.to_s) }
before do
visit edit_project_issue_path(project, issue)
end
it 'saves with due date' do
date = Date.today.at_beginning_of_month
expect(find('#issuable-due-date').value).to eq date.to_s
date = date.tomorrow
fill_in 'issue_title', with: 'bug 345'
fill_in 'issue_description', with: 'bug description'
find('#issuable-due-date').click
page.within '.pika-single' do
click_button date.day
end
expect(find('#issuable-due-date').value).to eq date.to_s
click_button 'Save changes'
page.within '.issuable-sidebar' do
expect(page).to have_content date.to_s(:medium)
end
end
it 'warns about version conflict' do
issue.update(title: "New title")
fill_in 'issue_title', with: 'bug 345'
fill_in 'issue_description', with: 'bug description'
click_button 'Save changes'
expect(page).to have_content 'Someone edited the issue the same time you did'
end
end
end
......
......@@ -149,7 +149,7 @@ feature 'Project > Members > Share with Group', :js do
create(:group).add_owner(master)
visit project_settings_members_path(project)
execute_script 'GroupsSelect.PER_PAGE = 1;'
execute_script 'GROUP_SELECT_PER_PAGE = 1;'
open_select2 '#link_group_id'
end
......
......@@ -181,6 +181,21 @@ describe "Internal Project Access" do
it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/issues/:id/edit" do
let(:issue) { create(:issue, project: project) }
subject { edit_project_issue_path(project, issue) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
it { is_expected.to be_allowed_for(:master).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
it { is_expected.to be_denied_for(:user) }
it { is_expected.to be_denied_for(:external) }
it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/snippets" do
subject { project_snippets_path(project) }
......
......@@ -181,6 +181,21 @@ describe "Private Project Access" do
it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/issues/:id/edit" do
let(:issue) { create(:issue, project: project) }
subject { edit_project_issue_path(project, issue) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
it { is_expected.to be_allowed_for(:master).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
it { is_expected.to be_denied_for(:user) }
it { is_expected.to be_denied_for(:external) }
it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/snippets" do
subject { project_snippets_path(project) }
......
......@@ -394,6 +394,21 @@ describe "Public Project Access" do
it { is_expected.to be_allowed_for(:visitor) }
end
describe "GET /:project_path/issues/:id/edit" do
let(:issue) { create(:issue, project: project) }
subject { edit_project_issue_path(project, issue) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
it { is_expected.to be_allowed_for(:master).of(project) }
it { is_expected.to be_allowed_for(:developer).of(project) }
it { is_expected.to be_allowed_for(:reporter).of(project) }
it { is_expected.to be_denied_for(:guest).of(project) }
it { is_expected.to be_denied_for(:user) }
it { is_expected.to be_denied_for(:external) }
it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/snippets" do
subject { project_snippets_path(project) }
......
......@@ -46,6 +46,15 @@ describe BranchesFinder do
expect(result.count).to eq(1)
end
it 'filters branches by name ignoring letter case' do
branches_finder = described_class.new(repository, { search: 'FiX' })
result = branches_finder.execute
expect(result.first.name).to eq('fix')
expect(result.count).to eq(1)
end
it 'does not find any branch with that name' do
branches_finder = described_class.new(repository, { search: 'random' })
......
require 'spec_helper'
describe InstanceConfigurationHelper do
describe '#instance_configuration_cell_html' do
describe 'if not block is passed' do
it 'returns the parameter if present' do
expect(helper.instance_configuration_cell_html('gitlab')).to eq('gitlab')
end
it 'returns "-" if the parameter is blank' do
expect(helper.instance_configuration_cell_html(nil)).to eq('-')
expect(helper.instance_configuration_cell_html('')).to eq('-')
end
end
describe 'if a block is passed' do
let(:upcase_block) { ->(value) { value.upcase } }
it 'returns the result of the block' do
expect(helper.instance_configuration_cell_html('gitlab', &upcase_block)).to eq('GITLAB')
expect(helper.instance_configuration_cell_html('gitlab') { |v| v.upcase }).to eq('GITLAB')
end
it 'returns "-" if the parameter is blank' do
expect(helper.instance_configuration_cell_html(nil, &upcase_block)).to eq('-')
expect(helper.instance_configuration_cell_html(nil) { |v| v.upcase }).to eq('-')
expect(helper.instance_configuration_cell_html('', &upcase_block)).to eq('-')
end
end
it 'boolean are valid values to display' do
expect(helper.instance_configuration_cell_html(true)).to eq(true)
expect(helper.instance_configuration_cell_html(false)).to eq(false)
end
end
describe '#instance_configuration_human_size_cell' do
it 'returns "-" if the parameter is blank' do
expect(helper.instance_configuration_human_size_cell(nil)).to eq('-')
expect(helper.instance_configuration_human_size_cell('')).to eq('-')
end
it 'accepts the value in bytes' do
expect(helper.instance_configuration_human_size_cell(1024)).to eq('1 KB')
end
it 'returns the value in human size readable format' do
expect(helper.instance_configuration_human_size_cell(1048576)).to eq('1 MB')
end
end
end
......@@ -43,6 +43,10 @@ describe('MRWidgetReadyToMerge', () => {
vm = createComponent();
});
afterEach(() => {
vm.$destroy();
});
describe('props', () => {
it('should have props', () => {
const { mr, service } = readyToMergeComponent.props;
......@@ -495,6 +499,48 @@ describe('MRWidgetReadyToMerge', () => {
});
});
describe('Merge controls', () => {
describe('when allowed to merge', () => {
beforeEach(() => {
vm = createComponent({
mr: { isMergeAllowed: true },
});
});
it('shows remove source branch checkbox', () => {
expect(vm.$el.querySelector('.js-remove-source-branch-checkbox')).toBeDefined();
});
it('shows modify commit message button', () => {
expect(vm.$el.querySelector('.js-modify-commit-message-button')).toBeDefined();
});
it('does not show message about needing to resolve items', () => {
expect(vm.$el.querySelector('.js-resolve-mr-widget-items-message')).toBeNull();
});
});
describe('when not allowed to merge', () => {
beforeEach(() => {
vm = createComponent({
mr: { isMergeAllowed: false },
});
});
it('does not show remove source branch checkbox', () => {
expect(vm.$el.querySelector('.js-remove-source-branch-checkbox')).toBeNull();
});
it('does not show modify commit message button', () => {
expect(vm.$el.querySelector('.js-modify-commit-message-button')).toBeNull();
});
it('shows message to resolve all items before being allowed to merge', () => {
expect(vm.$el.querySelector('.js-resolve-mr-widget-items-message')).toBeDefined();
});
});
});
describe('Commit message area', () => {
it('when using merge commits, should show "Modify commit message" button', () => {
const customVm = createComponent({
......
import Vue from 'vue';
import issuePlaceholderNote from '~/notes/components/issue_placeholder_note.vue';
import issuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import store from '~/notes/stores';
import { userDataMock } from '../mock_data';
import { userDataMock } from '../../../notes/mock_data';
describe('issue placeholder system note component', () => {
let vm;
......
import Vue from 'vue';
import placeholderSystemNote from '~/notes/components/issue_placeholder_system_note.vue';
import placeholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('placeholder system note component', () => {
let PlaceholderSystemNote;
let vm;
describe('issue placeholder system note component', () => {
let mountComponent;
beforeEach(() => {
const PlaceholderSystemNote = Vue.extend(placeholderSystemNote);
PlaceholderSystemNote = Vue.extend(placeholderSystemNote);
});
mountComponent = props => new PlaceholderSystemNote({
propsData: {
note: {
body: props,
},
},
}).$mount();
afterEach(() => {
vm.$destroy();
});
it('should render system note placeholder with plain text', () => {
const vm = mountComponent('This is a placeholder');
vm = mountComponent(PlaceholderSystemNote, {
note: { body: 'This is a placeholder' },
});
expect(vm.$el.tagName).toEqual('LI');
expect(vm.$el.querySelector('.timeline-content em').textContent.trim()).toEqual('This is a placeholder');
......
import Vue from 'vue';
import issueSystemNote from '~/notes/components/issue_system_note.vue';
import issueSystemNote from '~/vue_shared/components/notes/system_note.vue';
import store from '~/notes/stores';
describe('issue system note', () => {
......@@ -33,6 +33,10 @@ describe('issue system note', () => {
}).$mount();
});
afterEach(() => {
vm.$destroy();
});
it('should render a list item with correct id', () => {
expect(vm.$el.getAttribute('id')).toEqual(`note_${props.note.id}`);
});
......
......@@ -11,6 +11,7 @@ describe('User Avatar Link Component', function () {
imgCssClasses: 'myextraavatarclass',
tooltipText: 'tooltip text',
tooltipPlacement: 'bottom',
username: 'username',
};
const UserAvatarLinkComponent = Vue.extend(UserAvatarLink);
......@@ -47,4 +48,42 @@ describe('User Avatar Link Component', function () {
expect(this.userAvatarLink[key]).toBeDefined();
});
});
describe('no username', function () {
beforeEach(function (done) {
this.userAvatarLink.username = '';
Vue.nextTick(done);
});
it('should only render image tag in link', function () {
const childElements = this.userAvatarLink.$el.childNodes;
expect(childElements[0].tagName).toBe('IMG');
// Vue will render the hidden component as <!---->
expect(childElements[1].tagName).toBeUndefined();
});
it('should render avatar image tooltip', function () {
expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual(this.propsData.tooltipText);
});
});
describe('username', function () {
it('should not render avatar image tooltip', function () {
expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual('');
});
it('should render username prop in <span>', function () {
expect(this.userAvatarLink.$el.querySelector('span').innerText.trim()).toEqual(this.propsData.username);
});
it('should render text tooltip for <span>', function () {
expect(this.userAvatarLink.$el.querySelector('span').dataset.originalTitle).toEqual(this.propsData.tooltipText);
});
it('should render text tooltip placement for <span>', function () {
expect(this.userAvatarLink.$el.querySelector('span').getAttribute('tooltip-placement')).toEqual(this.propsData.tooltipPlacement);
});
});
});
......@@ -15,9 +15,13 @@ describe Banzai::Filter::GollumTagsFilter do
context 'linking internal images' do
it 'creates img tag if image exists' do
file = Gollum::File.new(project_wiki.wiki)
expect(file).to receive(:path).and_return('images/image.jpg')
expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(file)
gollum_file_double = double('Gollum::File',
mime_type: 'image/jpeg',
name: 'images/image.jpg',
path: 'images/image.jpg',
raw_data: '')
wiki_file = Gitlab::Git::WikiFile.new(gollum_file_double)
expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(wiki_file)
tag = '[[images/image.jpg]]'
doc = filter("See #{tag}", project_wiki: project_wiki)
......
......@@ -7,6 +7,38 @@ describe Gitlab::Git::Branch, seed_helper: true do
it { is_expected.to be_kind_of Array }
describe '.find' do
subject { described_class.find(repository, branch) }
before do
allow(repository).to receive(:find_branch).with(branch)
.and_call_original
end
context 'when finding branch via branch name' do
let(:branch) { 'master' }
it 'returns a branch object' do
expect(subject).to be_a(described_class)
expect(subject.name).to eq(branch)
expect(repository).to have_received(:find_branch).with(branch)
end
end
context 'when the branch is already a branch' do
let(:commit) { repository.commit('master') }
let(:branch) { described_class.new(repository, 'master', commit.sha, commit) }
it 'returns a branch object' do
expect(subject).to be_a(described_class)
expect(subject).to eq(branch)
expect(repository).not_to have_received(:find_branch).with(branch)
end
end
end
describe '#size' do
subject { super().size }
it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) }
......
require 'spec_helper'
describe Gitlab::Git::HooksService, seed_helper: true do
let(:user) { Gitlab::Git::User.new('janedoe', 'Jane Doe', 'janedoe@example.com', 'user-456') }
let(:gl_id) { 'user-456' }
let(:gl_username) { 'janedoe' }
let(:user) { Gitlab::Git::User.new(gl_username, 'Jane Doe', 'janedoe@example.com', gl_id) }
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, 'project-123') }
let(:service) { described_class.new }
before do
@blankrev = Gitlab::Git::BLANK_SHA
@oldrev = SeedRepo::Commit::PARENT_ID
@newrev = SeedRepo::Commit::ID
@ref = 'refs/heads/feature'
end
let(:blankrev) { Gitlab::Git::BLANK_SHA }
let(:oldrev) { SeedRepo::Commit::PARENT_ID }
let(:newrev) { SeedRepo::Commit::ID }
let(:ref) { 'refs/heads/feature' }
describe '#execute' do
context 'when receive hooks were successful' do
it 'calls post-receive hook' do
hook = double(trigger: [true, nil])
let(:hook) { double(:hook) }
it 'calls all three hooks' do
expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
expect(hook).to receive(:trigger).with(gl_id, gl_username, blankrev, newrev, ref)
.exactly(3).times.and_return([true, nil])
service.execute(user, repository, @blankrev, @newrev, @ref) { }
service.execute(user, repository, blankrev, newrev, ref) { }
end
end
......@@ -28,7 +30,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do
expect(service).not_to receive(:run_hook).with('post-receive')
expect do
service.execute(user, repository, @blankrev, @newrev, @ref)
service.execute(user, repository, blankrev, newrev, ref)
end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
end
end
......@@ -40,7 +42,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do
expect(service).not_to receive(:run_hook).with('post-receive')
expect do
service.execute(user, repository, @blankrev, @newrev, @ref)
service.execute(user, repository, blankrev, newrev, ref)
end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
end
end
......
......@@ -1135,6 +1135,32 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
describe '#merged_branch_names' do
context 'when branch names are passed' do
it 'only returns the names we are asking' do
names = repository.merged_branch_names(%w[merge-test])
expect(names).to contain_exactly('merge-test')
end
it 'does not return unmerged branch names' do
names = repository.merged_branch_names(%w[feature])
expect(names).to be_empty
end
end
context 'when no branch names are specified' do
it 'returns all merged branch names' do
names = repository.merged_branch_names
expect(names).to include('merge-test')
expect(names).to include('fix-mode')
expect(names).not_to include('feature')
end
end
end
describe "#ls_files" do
let(:master_file_paths) { repository.ls_files("master") }
let(:not_existed_branch) { repository.ls_files("not_existed_branch") }
......@@ -1564,6 +1590,60 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
describe '#ff_merge' do
let(:repository) do
Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
end
let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }
let(:user) { build(:user) }
let(:target_branch) { 'test-ff-target-branch' }
before do
repository.create_branch(target_branch, branch_head)
end
after do
ensure_seeds
end
subject { repository.ff_merge(user, source_sha, target_branch) }
it 'performs a ff_merge' do
expect(subject.newrev).to eq(source_sha)
expect(subject.repo_created).to be(false)
expect(subject.branch_created).to be(false)
expect(repository.commit(target_branch).id).to eq(source_sha)
end
context 'with a non-existing target branch' do
subject { repository.ff_merge(user, source_sha, 'this-isnt-real') }
it 'throws an ArgumentError' do
expect { subject }.to raise_error(ArgumentError)
end
end
context 'with a non-existing source commit' do
let(:source_sha) { 'f001' }
it 'throws an ArgumentError' do
expect { subject }.to raise_error(ArgumentError)
end
end
context 'when the source sha is not a descendant of the branch head' do
let(:source_sha) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' }
it "doesn't perform the ff_merge" do
expect { subject }.to raise_error(Gitlab::Git::CommitError)
expect(repository.commit(target_branch).id).to eq(branch_head)
end
end
end
def create_remote_branch(repository, remote_name, branch_name, source_branch_name)
source_branch = repository.branches.find { |branch| branch.name == source_branch_name }
rugged = repository.rugged
......
......@@ -40,4 +40,12 @@ describe Email do
expect(user.emails.confirmed.count).to eq 1
end
end
describe 'delegation' do
let(:user) { create(:user) }
it 'delegates to :user' do
expect(build(:email, user: user).username).to eq user.username
end
end
end
require 'spec_helper'
RSpec.describe InstanceConfiguration do
context 'without cache' do
describe '#settings' do
describe '#ssh_algorithms_hashes' do
let(:md5) { '54:e0:f8:70:d6:4f:4c:b1:b3:02:44:77:cf:cd:0d:fc' }
let(:sha256) { '9327f0d15a48c4d9f6a3aee65a1825baf9a3412001c98169c5fd022ac27762fc' }
it 'does not return anything if file does not exist' do
stub_pub_file(exist: false)
expect(subject.settings[:ssh_algorithms_hashes]).to be_empty
end
it 'does not return anything if file is empty' do
stub_pub_file
allow(File).to receive(:read).and_return('')
expect(subject.settings[:ssh_algorithms_hashes]).to be_empty
end
it 'returns the md5 and sha256 if file valid and exists' do
stub_pub_file
result = subject.settings[:ssh_algorithms_hashes].select { |o| o[:md5] == md5 && o[:sha256] == sha256 }
expect(result.size).to eq(InstanceConfiguration::SSH_ALGORITHMS.size)
end
def stub_pub_file(exist: true)
path = 'spec/fixtures/ssh_host_example_key.pub'
path << 'random' unless exist
allow(subject).to receive(:ssh_algorithm_file).and_return(Rails.root.join(path))
end
end
describe '#host' do
it 'returns current instance host' do
allow(Settings.gitlab).to receive(:host).and_return('exampledomain')
expect(subject.settings[:host]).to eq(Settings.gitlab.host)
end
end
describe '#gitlab_pages' do
let(:gitlab_pages) { subject.settings[:gitlab_pages] }
it 'returns Settings.pages' do
gitlab_pages.delete(:ip_address)
expect(gitlab_pages).to eq(Settings.pages.symbolize_keys)
end
it 'returns the Gitlab\'s pages host ip address' do
expect(gitlab_pages.keys).to include(:ip_address)
end
it 'returns the ip address as nil if the domain is invalid' do
allow(Settings.pages).to receive(:host).and_return('exampledomain')
expect(gitlab_pages[:ip_address]).to eq nil
end
it 'returns the ip address of the domain' do
allow(Settings.pages).to receive(:host).and_return('localhost')
expect(gitlab_pages[:ip_address]).to eq('127.0.0.1').or eq('::1')
end
end
describe '#gitlab_ci' do
let(:gitlab_ci) { subject.settings[:gitlab_ci] }
it 'returns Settings.gitalb_ci' do
gitlab_ci.delete(:artifacts_max_size)
expect(gitlab_ci).to eq(Settings.gitlab_ci.symbolize_keys)
end
it 'returns the key artifacts_max_size' do
expect(gitlab_ci.keys).to include(:artifacts_max_size)
end
end
end
end
context 'with cache', :use_clean_rails_memory_store_caching do
it 'caches settings content' do
expect(Rails.cache.read(described_class::CACHE_KEY)).to be_nil
settings = subject.settings
expect(Rails.cache.read(described_class::CACHE_KEY)).to eq(settings)
end
describe 'cached settings' do
before do
subject.settings
end
it 'expires after EXPIRATION_TIME' do
allow(Time).to receive(:now).and_return(Time.now + described_class::EXPIRATION_TIME)
Rails.cache.cleanup
expect(Rails.cache.read(described_class::CACHE_KEY)).to eq(nil)
end
end
end
end
......@@ -86,7 +86,7 @@ describe MergeRequest do
context 'when the target branch does not exist' do
before do
project.repository.raw_repository.delete_branch(subject.target_branch)
project.repository.rm_branch(subject.author, subject.target_branch)
end
it 'returns nil' do
......@@ -1388,7 +1388,7 @@ describe MergeRequest do
context 'when the target branch does not exist' do
before do
subject.project.repository.raw_repository.delete_branch(subject.target_branch)
subject.project.repository.rm_branch(subject.author, subject.target_branch)
end
it 'returns nil' do
......@@ -1460,6 +1460,12 @@ describe MergeRequest do
end
describe '#merge_ongoing?' do
it 'returns true when the merge request is locked' do
merge_request = build_stubbed(:merge_request, state: :locked)
expect(merge_request.merge_ongoing?).to be(true)
end
it 'returns true when merge_id, MR is not merged and it has no running job' do
merge_request = build_stubbed(:merge_request, state: :open, merge_jid: 'foo')
allow(Gitlab::SidekiqStatus).to receive(:running?).with('foo') { true }
......
......@@ -24,6 +24,8 @@ describe JiraService do
end
it { is_expected.not_to validate_presence_of(:url) }
it { is_expected.not_to validate_presence_of(:username) }
it { is_expected.not_to validate_presence_of(:password) }
end
context 'validating urls' do
......@@ -54,6 +56,18 @@ describe JiraService do
expect(service).not_to be_valid
end
it 'is not valid when username is missing' do
service.username = nil
expect(service).not_to be_valid
end
it 'is not valid when password is missing' do
service.password = nil
expect(service).not_to be_valid
end
it 'is valid when api url is a valid url' do
service.api_url = 'http://jira.test.com/api'
......
......@@ -99,45 +99,34 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
describe '#actual_namespace' do
subject { service.actual_namespace }
it "returns the default namespace" do
is_expected.to eq(service.send(:default_namespace))
end
context 'when namespace is specified' do
before do
service.namespace = 'my-namespace'
shared_examples 'a correctly formatted namespace' do
it 'returns a valid Kubernetes namespace name' do
expect(subject).to match(Gitlab::Regex.kubernetes_namespace_regex)
expect(subject).to eq(expected_namespace)
end
end
it "returns the user-namespace" do
is_expected.to eq('my-namespace')
end
it_behaves_like 'a correctly formatted namespace' do
let(:expected_namespace) { service.send(:default_namespace) }
end
context 'when service is not assigned to project' do
context 'when the project path contains forbidden characters' do
before do
service.project = nil
project.path = '-a_Strange.Path--forSure'
end
it "does not return namespace" do
is_expected.to be_nil
it_behaves_like 'a correctly formatted namespace' do
let(:expected_namespace) { "a-strange-path--forsure-#{project.id}" }
end
end
end
describe '#actual_namespace' do
subject { service.actual_namespace }
it "returns the default namespace" do
is_expected.to eq(service.send(:default_namespace))
end
context 'when namespace is specified' do
before do
service.namespace = 'my-namespace'
end
it "returns the user-namespace" do
is_expected.to eq('my-namespace')
it_behaves_like 'a correctly formatted namespace' do
let(:expected_namespace) { 'my-namespace' }
end
end
......@@ -146,7 +135,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
service.project = nil
end
it "does not return namespace" do
it 'does not return namespace' do
is_expected.to be_nil
end
end
......
......@@ -299,6 +299,24 @@ describe Repository do
it { is_expected.to be_falsey }
end
context 'when pre-loaded merged branches are provided' do
using RSpec::Parameterized::TableSyntax
where(:branch, :pre_loaded, :expected) do
'not-merged-branch' | ['branch-merged'] | false
'branch-merged' | ['not-merged-branch'] | false
'branch-merged' | ['branch-merged'] | true
'not-merged-branch' | ['not-merged-branch'] | false
'master' | ['master'] | false
end
with_them do
subject { repository.merged_to_root_ref?(branch, pre_loaded) }
it { is_expected.to eq(expected) }
end
end
end
describe '#can_be_merged?' do
......@@ -2110,19 +2128,41 @@ describe Repository do
end
describe '#cache_method_output', :use_clean_rails_memory_store_caching do
let(:fallback) { 10 }
context 'with a non-existing repository' do
let(:value) do
repository.cache_method_output(:cats, fallback: 10) do
raise Rugged::ReferenceError
let(:project) { create(:project) } # No repository
subject do
repository.cache_method_output(:cats, fallback: fallback) do
repository.cats_call_stub
end
end
it 'returns a fallback value' do
expect(value).to eq(10)
it 'returns the fallback value' do
expect(subject).to eq(fallback)
end
it 'avoids calling the original method' do
expect(repository).not_to receive(:cats_call_stub)
subject
end
end
context 'with a method throwing a non-existing-repository error' do
subject do
repository.cache_method_output(:cats, fallback: fallback) do
raise Gitlab::Git::Repository::NoRepository
end
end
it 'returns the fallback value' do
expect(subject).to eq(fallback)
end
it 'does not cache the data' do
value
subject
expect(repository.instance_variable_defined?(:@cats)).to eq(false)
expect(repository.send(:cache).exist?(:cats)).to eq(false)
......@@ -2238,4 +2278,24 @@ describe Repository do
end
end
end
describe 'commit cache' do
set(:project) { create(:project, :repository) }
it 'caches based on SHA' do
# Gets the commit oid, and warms the cache
oid = project.commit.id
expect(Gitlab::Git::Commit).not_to receive(:find).once
project.commit_by(oid: oid)
end
it 'caches nil values' do
expect(Gitlab::Git::Commit).to receive(:find).once
project.commit_by(oid: '1' * 40)
project.commit_by(oid: '1' * 40)
end
end
end
......@@ -385,7 +385,7 @@ describe API::Runner do
end
context 'when job is made for tag' do
let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
it 'sets branch as ref_type' do
request_job
......@@ -436,8 +436,8 @@ describe API::Runner do
end
context 'when project and pipeline have multiple jobs' do
let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
before do
......@@ -458,7 +458,7 @@ describe API::Runner do
end
context 'when pipeline have jobs with artifacts' do
let!(:job) { create(:ci_build_tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:job) { create(:ci_build, :tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
before do
......@@ -478,8 +478,8 @@ describe API::Runner do
end
context 'when explicit dependencies are defined' do
let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:test_job) do
create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'deploy',
stage: 'deploy', stage_idx: 1,
......@@ -502,8 +502,8 @@ describe API::Runner do
end
context 'when dependencies is an empty array' do
let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
let!(:job2) { create(:ci_build, :tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:empty_dependencies_job) do
create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'empty_dependencies_job',
stage: 'deploy', stage_idx: 1,
......
......@@ -12,55 +12,6 @@ describe MergeRequests::MergeService do
end
describe '#execute' do
context 'MergeRequest#merge_jid' do
let(:service) do
described_class.new(project, user, commit_message: 'Awesome message')
end
before do
merge_request.update_column(:merge_jid, 'hash-123')
end
it 'is cleaned when no error is raised' do
service.execute(merge_request)
expect(merge_request.reload.merge_jid).to be_nil
end
it 'is cleaned when expected error is raised' do
allow(service).to receive(:commit).and_raise(described_class::MergeError)
service.execute(merge_request)
expect(merge_request.reload.merge_jid).to be_nil
end
it 'is cleaned when merge request is not mergeable' do
allow(merge_request).to receive(:mergeable?).and_return(false)
service.execute(merge_request)
expect(merge_request.reload.merge_jid).to be_nil
end
it 'is cleaned when no source is found' do
allow(merge_request).to receive(:diff_head_sha).and_return(nil)
service.execute(merge_request)
expect(merge_request.reload.merge_jid).to be_nil
end
it 'is not cleaned when unexpected error is raised' do
service = described_class.new(project, user, commit_message: 'Awesome message')
allow(service).to receive(:commit).and_raise(StandardError)
expect { service.execute(merge_request) }.to raise_error(StandardError)
expect(merge_request.reload.merge_jid).to be_present
end
end
context 'valid params' do
let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
......
......@@ -63,6 +63,12 @@ describe SystemHooksService do
:group_id, :user_id, :user_username, :user_name, :user_email, :group_access
)
end
it 'includes the correct project visibility level' do
data = event_data(project, :create)
expect(data[:project_visibility]).to eq('private')
end
end
context 'event names' do
......
......@@ -6,6 +6,8 @@ module JiraServiceHelper
properties = {
title: "JIRA tracker",
url: JIRA_URL,
username: 'jira-user',
password: 'my-secret-password',
project_key: "JIRA",
jira_issue_transition_id: '1'
}
......
......@@ -25,13 +25,11 @@ shared_examples 'update invalid issuable' do |klass|
.and_raise(ActiveRecord::StaleObjectError.new(issuable, :save))
end
if klass == MergeRequest
it 'renders edit when format is html' do
put :update, params
it 'renders edit when format is html' do
put :update, params
expect(response).to render_template(:edit)
expect(assigns[:conflict]).to be_truthy
end
expect(response).to render_template(:edit)
expect(assigns[:conflict]).to be_truthy
end
it 'renders json error message when format is json' do
......@@ -44,17 +42,16 @@ shared_examples 'update invalid issuable' do |klass|
end
end
if klass == MergeRequest
context 'when updating an invalid issuable' do
before do
params[:merge_request][:title] = ""
end
context 'when updating an invalid issuable' do
before do
key = klass == Issue ? :issue : :merge_request
params[key][:title] = ""
end
it 'renders edit when merge request is invalid' do
put :update, params
it 'renders edit when merge request is invalid' do
put :update, params
expect(response).to render_template(:edit)
end
expect(response).to render_template(:edit)
end
end
end
......@@ -25,6 +25,14 @@ describe 'help/index' do
end
end
describe 'instance configuration link' do
it 'is visible to guests' do
render
expect(rendered).to have_link(nil, help_instance_configuration_url)
end
end
def stub_user(user = double)
allow(view).to receive(:user_signed_in?).and_return(user)
end
......
require 'rails_helper'
describe 'help/instance_configuration' do
describe 'General Sections:' do
let(:instance_configuration) { build(:instance_configuration)}
let(:settings) { instance_configuration.settings }
let(:ssh_settings) { settings[:ssh_algorithms_hashes] }
before do
assign(:instance_configuration, instance_configuration)
end
it 'has links to several sections' do
render
expect(rendered).to have_link(nil, '#ssh-host-keys-fingerprints') if ssh_settings.any?
expect(rendered).to have_link(nil, '#gitlab-pages')
expect(rendered).to have_link(nil, '#gitlab-ci')
end
it 'has several sections' do
render
expect(rendered).to have_css('h2#ssh-host-keys-fingerprints') if ssh_settings.any?
expect(rendered).to have_css('h2#gitlab-pages')
expect(rendered).to have_css('h2#gitlab-ci')
end
end
end
......@@ -12,8 +12,13 @@ describe StuckMergeJobsWorker do
worker.perform
expect(mr_with_sha.reload).to be_merged
expect(mr_without_sha.reload).to be_opened
mr_with_sha.reload
mr_without_sha.reload
expect(mr_with_sha).to be_merged
expect(mr_without_sha).to be_opened
expect(mr_with_sha.merge_jid).to be_present
expect(mr_without_sha.merge_jid).to be_nil
end
it 'updates merge request to opened when locked but has not been merged' do
......
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