Merge branch 'master' into multi-file-editor-dirty-diff-indicator
This commit is contained in:
commit
386cbf22ee
|
|
@ -579,7 +579,7 @@ codequality:
|
|||
script:
|
||||
- cp .rubocop.yml .rubocop.yml.bak
|
||||
- grep -v "rubocop-gitlab-security" .rubocop.yml.bak > .rubocop.yml
|
||||
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate:0.69.0 analyze -f json > raw_codeclimate.json
|
||||
- docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json
|
||||
- cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json
|
||||
- mv .rubocop.yml.bak .rubocop.yml
|
||||
artifacts:
|
||||
|
|
|
|||
|
|
@ -1153,7 +1153,7 @@ DEPENDENCIES
|
|||
sanitize (~> 2.0)
|
||||
sass-rails (~> 5.0.6)
|
||||
scss_lint (~> 0.54.0)
|
||||
seed-fu (~> 2.3.5)
|
||||
seed-fu (~> 2.3.7)
|
||||
select2-rails (~> 3.5.9)
|
||||
selenium-webdriver (~> 3.5)
|
||||
sentry-raven (~> 2.5.3)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
import Clipboard from 'clipboard';
|
||||
|
||||
function showTooltip(target, title) {
|
||||
const $target = $(target);
|
||||
const originalTitle = $target.data('original-title');
|
||||
|
||||
if (!$target.data('hideTooltip')) {
|
||||
$target
|
||||
.attr('title', title)
|
||||
.tooltip('fixTitle')
|
||||
.tooltip('show')
|
||||
.attr('title', originalTitle)
|
||||
.tooltip('fixTitle');
|
||||
}
|
||||
}
|
||||
|
||||
function genericSuccess(e) {
|
||||
showTooltip(e.trigger, 'Copied');
|
||||
// Clear the selection and blur the trigger so it loses its border
|
||||
e.clearSelection();
|
||||
$(e.trigger).blur();
|
||||
}
|
||||
|
||||
/**
|
||||
* Safari > 10 doesn't support `execCommand`, so instead we inform the user to copy manually.
|
||||
* See http://clipboardjs.com/#browser-support
|
||||
*/
|
||||
function genericError(e) {
|
||||
let key;
|
||||
if (/Mac/i.test(navigator.userAgent)) {
|
||||
key = '⌘'; // Command
|
||||
} else {
|
||||
key = 'Ctrl';
|
||||
}
|
||||
showTooltip(e.trigger, `Press ${key}-C to copy`);
|
||||
}
|
||||
|
||||
export default function initCopyToClipboard() {
|
||||
const clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
|
||||
clipboard.on('success', genericSuccess);
|
||||
clipboard.on('error', genericError);
|
||||
|
||||
/**
|
||||
* This a workaround around ClipboardJS limitations to allow the context-specific copy/pasting
|
||||
* of plain text or GFM. The Ruby `clipboard_button` helper sneaks a JSON hash with `text` and
|
||||
* `gfm` keys into the `data-clipboard-text` attribute that ClipboardJS reads from.
|
||||
* When ClipboardJS creates a new `textarea` (directly inside `body`, with a `readonly`
|
||||
* attribute`), sets its value to the value of this data attribute, focusses on it, and finally
|
||||
* programmatically issues the 'Copy' command, this code intercepts the copy command/event at
|
||||
* the last minute to deconstruct this JSON hash and set the `text/plain` and `text/x-gfm` copy
|
||||
* data types to the intended values.
|
||||
*/
|
||||
$(document).on('copy', 'body > textarea[readonly]', (e) => {
|
||||
const clipboardData = e.originalEvent.clipboardData;
|
||||
if (!clipboardData) return;
|
||||
|
||||
const text = e.target.value;
|
||||
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(text);
|
||||
} catch (ex) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json.text || !json.gfm) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
clipboardData.setData('text/plain', json.text);
|
||||
clipboardData.setData('text/x-gfm', json.gfm);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import './autosize';
|
||||
import './bind_in_out';
|
||||
import initCopyAsGFM from './copy_as_gfm';
|
||||
import initCopyToClipboard from './copy_to_clipboard';
|
||||
import './details_behavior';
|
||||
import installGlEmojiElement from './gl_emoji';
|
||||
import './quick_submit';
|
||||
|
|
@ -9,3 +10,4 @@ import './toggler_behavior';
|
|||
|
||||
installGlEmojiElement();
|
||||
initCopyAsGFM();
|
||||
initCopyToClipboard();
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, max-len */
|
||||
|
||||
import Clipboard from 'vendor/clipboard';
|
||||
|
||||
var genericError, genericSuccess, showTooltip;
|
||||
|
||||
genericSuccess = function(e) {
|
||||
showTooltip(e.trigger, 'Copied');
|
||||
// Clear the selection and blur the trigger so it loses its border
|
||||
e.clearSelection();
|
||||
return $(e.trigger).blur();
|
||||
};
|
||||
|
||||
// Safari doesn't support `execCommand`, so instead we inform the user to
|
||||
// copy manually.
|
||||
//
|
||||
// See http://clipboardjs.com/#browser-support
|
||||
genericError = function(e) {
|
||||
var key;
|
||||
if (/Mac/i.test(navigator.userAgent)) {
|
||||
key = '⌘'; // Command
|
||||
} else {
|
||||
key = 'Ctrl';
|
||||
}
|
||||
return showTooltip(e.trigger, "Press " + key + "-C to copy");
|
||||
};
|
||||
|
||||
showTooltip = function(target, title) {
|
||||
var $target = $(target);
|
||||
var originalTitle = $target.data('original-title');
|
||||
|
||||
if (!$target.data('hideTooltip')) {
|
||||
$target
|
||||
.attr('title', 'Copied')
|
||||
.tooltip('fixTitle')
|
||||
.tooltip('show')
|
||||
.attr('title', originalTitle)
|
||||
.tooltip('fixTitle');
|
||||
}
|
||||
};
|
||||
|
||||
$(function() {
|
||||
const clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
|
||||
clipboard.on('success', genericSuccess);
|
||||
clipboard.on('error', genericError);
|
||||
|
||||
// This a workaround around ClipboardJS limitations to allow the context-specific copy/pasting of plain text or GFM.
|
||||
// The Ruby `clipboard_button` helper sneaks a JSON hash with `text` and `gfm` keys into the `data-clipboard-text`
|
||||
// attribute that ClipboardJS reads from.
|
||||
// When ClipboardJS creates a new `textarea` (directly inside `body`, with a `readonly` attribute`), sets its value
|
||||
// to the value of this data attribute, focusses on it, and finally programmatically issues the 'Copy' command,
|
||||
// this code intercepts the copy command/event at the last minute to deconstruct this JSON hash and set the
|
||||
// `text/plain` and `text/x-gfm` copy data types to the intended values.
|
||||
$(document).on('copy', 'body > textarea[readonly]', function(e) {
|
||||
const clipboardData = e.originalEvent.clipboardData;
|
||||
if (!clipboardData) return;
|
||||
|
||||
const text = e.target.value;
|
||||
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(text);
|
||||
} catch (ex) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json.text || !json.gfm) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
clipboardData.setData('text/plain', json.text);
|
||||
clipboardData.setData('text/x-gfm', json.gfm);
|
||||
});
|
||||
});
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
import Flash from './flash';
|
||||
import DropLab from './droplab/drop_lab';
|
||||
import ISetter from './droplab/plugins/input_setter';
|
||||
import { __, sprintf } from './locale';
|
||||
|
||||
// Todo: Remove this when fixing issue in input_setter plugin
|
||||
const InputSetter = Object.assign({}, ISetter);
|
||||
|
|
@ -12,70 +13,63 @@ const CREATE_BRANCH = 'create-branch';
|
|||
export default class CreateMergeRequestDropdown {
|
||||
constructor(wrapperEl) {
|
||||
this.wrapperEl = wrapperEl;
|
||||
this.createMergeRequestButton = this.wrapperEl.querySelector('.js-create-merge-request');
|
||||
this.dropdownToggle = this.wrapperEl.querySelector('.js-dropdown-toggle');
|
||||
this.dropdownList = this.wrapperEl.querySelector('.dropdown-menu');
|
||||
this.availableButton = this.wrapperEl.querySelector('.available');
|
||||
this.branchInput = this.wrapperEl.querySelector('.js-branch-name');
|
||||
this.branchMessage = this.wrapperEl.querySelector('.js-branch-message');
|
||||
this.createMergeRequestButton = this.wrapperEl.querySelector('.js-create-merge-request');
|
||||
this.createTargetButton = this.wrapperEl.querySelector('.js-create-target');
|
||||
this.dropdownList = this.wrapperEl.querySelector('.dropdown-menu');
|
||||
this.dropdownToggle = this.wrapperEl.querySelector('.js-dropdown-toggle');
|
||||
this.refInput = this.wrapperEl.querySelector('.js-ref');
|
||||
this.refMessage = this.wrapperEl.querySelector('.js-ref-message');
|
||||
this.unavailableButton = this.wrapperEl.querySelector('.unavailable');
|
||||
this.unavailableButtonArrow = this.unavailableButton.querySelector('.fa');
|
||||
this.unavailableButtonText = this.unavailableButton.querySelector('.text');
|
||||
|
||||
this.createBranchPath = this.wrapperEl.dataset.createBranchPath;
|
||||
this.branchCreated = false;
|
||||
this.branchIsValid = true;
|
||||
this.canCreatePath = this.wrapperEl.dataset.canCreatePath;
|
||||
this.createBranchPath = this.wrapperEl.dataset.createBranchPath;
|
||||
this.createMrPath = this.wrapperEl.dataset.createMrPath;
|
||||
this.droplabInitialized = false;
|
||||
this.isCreatingMergeRequest = false;
|
||||
this.mergeRequestCreated = false;
|
||||
this.isCreatingBranch = false;
|
||||
this.branchCreated = false;
|
||||
this.isCreatingMergeRequest = false;
|
||||
this.isGettingRef = false;
|
||||
this.mergeRequestCreated = false;
|
||||
this.refDebounce = _.debounce((value, target) => this.getRef(value, target), 500);
|
||||
this.refIsValid = true;
|
||||
this.refsPath = this.wrapperEl.dataset.refsPath;
|
||||
this.suggestedRef = this.refInput.value;
|
||||
|
||||
// These regexps are used to replace
|
||||
// a backend generated new branch name and its source (ref)
|
||||
// with user's inputs.
|
||||
this.regexps = {
|
||||
branch: {
|
||||
createBranchPath: new RegExp('(branch_name=)(.+?)(?=&issue)'),
|
||||
createMrPath: new RegExp('(branch_name=)(.+?)(?=&ref)'),
|
||||
},
|
||||
ref: {
|
||||
createBranchPath: new RegExp('(ref=)(.+?)$'),
|
||||
createMrPath: new RegExp('(ref=)(.+?)$'),
|
||||
},
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.checkAbilityToCreateBranch();
|
||||
}
|
||||
|
||||
available() {
|
||||
this.availableButton.classList.remove('hide');
|
||||
this.unavailableButton.classList.add('hide');
|
||||
}
|
||||
|
||||
unavailable() {
|
||||
this.availableButton.classList.add('hide');
|
||||
this.unavailableButton.classList.remove('hide');
|
||||
}
|
||||
|
||||
enable() {
|
||||
this.createMergeRequestButton.classList.remove('disabled');
|
||||
this.createMergeRequestButton.removeAttribute('disabled');
|
||||
|
||||
this.dropdownToggle.classList.remove('disabled');
|
||||
this.dropdownToggle.removeAttribute('disabled');
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.createMergeRequestButton.classList.add('disabled');
|
||||
this.createMergeRequestButton.setAttribute('disabled', 'disabled');
|
||||
|
||||
this.dropdownToggle.classList.add('disabled');
|
||||
this.dropdownToggle.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.wrapperEl.classList.add('hide');
|
||||
}
|
||||
|
||||
setUnavailableButtonState(isLoading = true) {
|
||||
if (isLoading) {
|
||||
this.unavailableButtonArrow.classList.add('fa-spinner', 'fa-spin');
|
||||
this.unavailableButtonArrow.classList.remove('fa-exclamation-triangle');
|
||||
this.unavailableButtonText.textContent = 'Checking branch availability…';
|
||||
} else {
|
||||
this.unavailableButtonArrow.classList.remove('fa-spinner', 'fa-spin');
|
||||
this.unavailableButtonArrow.classList.add('fa-exclamation-triangle');
|
||||
this.unavailableButtonText.textContent = 'New branch unavailable';
|
||||
}
|
||||
bindEvents() {
|
||||
this.createMergeRequestButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
|
||||
this.createTargetButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
|
||||
this.branchInput.addEventListener('keyup', this.onChangeInput.bind(this));
|
||||
this.dropdownToggle.addEventListener('click', this.onClickSetFocusOnBranchNameInput.bind(this));
|
||||
this.refInput.addEventListener('keyup', this.onChangeInput.bind(this));
|
||||
this.refInput.addEventListener('keydown', CreateMergeRequestDropdown.processTab.bind(this));
|
||||
}
|
||||
|
||||
checkAbilityToCreateBranch() {
|
||||
|
|
@ -107,60 +101,18 @@ export default class CreateMergeRequestDropdown {
|
|||
});
|
||||
}
|
||||
|
||||
initDroplab() {
|
||||
this.droplab = new DropLab();
|
||||
|
||||
this.droplab.init(this.dropdownToggle, this.dropdownList, [InputSetter],
|
||||
this.getDroplabConfig());
|
||||
}
|
||||
|
||||
getDroplabConfig() {
|
||||
return {
|
||||
InputSetter: [{
|
||||
input: this.createMergeRequestButton,
|
||||
valueAttribute: 'data-value',
|
||||
inputAttribute: 'data-action',
|
||||
}, {
|
||||
input: this.createMergeRequestButton,
|
||||
valueAttribute: 'data-text',
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.createMergeRequestButton
|
||||
.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this));
|
||||
}
|
||||
|
||||
isBusy() {
|
||||
return this.isCreatingMergeRequest ||
|
||||
this.mergeRequestCreated ||
|
||||
this.isCreatingBranch ||
|
||||
this.branchCreated;
|
||||
}
|
||||
|
||||
onClickCreateMergeRequestButton(e) {
|
||||
let xhr = null;
|
||||
e.preventDefault();
|
||||
|
||||
if (this.isBusy()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.target.dataset.action === CREATE_MERGE_REQUEST) {
|
||||
xhr = this.createMergeRequest();
|
||||
} else if (e.target.dataset.action === CREATE_BRANCH) {
|
||||
xhr = this.createBranch();
|
||||
}
|
||||
|
||||
xhr.fail(() => {
|
||||
this.isCreatingMergeRequest = false;
|
||||
this.isCreatingBranch = false;
|
||||
});
|
||||
|
||||
xhr.always(() => this.enable());
|
||||
|
||||
this.disable();
|
||||
createBranch() {
|
||||
return $.ajax({
|
||||
method: 'POST',
|
||||
dataType: 'json',
|
||||
url: this.createBranchPath,
|
||||
beforeSend: () => (this.isCreatingBranch = true),
|
||||
})
|
||||
.done((data) => {
|
||||
this.branchCreated = true;
|
||||
window.location.href = data.url;
|
||||
})
|
||||
.fail(() => new Flash('Failed to create a branch for this issue. Please try again.'));
|
||||
}
|
||||
|
||||
createMergeRequest() {
|
||||
|
|
@ -177,17 +129,343 @@ export default class CreateMergeRequestDropdown {
|
|||
.fail(() => new Flash('Failed to create Merge Request. Please try again.'));
|
||||
}
|
||||
|
||||
createBranch() {
|
||||
disable() {
|
||||
this.disableCreateAction();
|
||||
|
||||
this.dropdownToggle.classList.add('disabled');
|
||||
this.dropdownToggle.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
|
||||
disableCreateAction() {
|
||||
this.createMergeRequestButton.classList.add('disabled');
|
||||
this.createMergeRequestButton.setAttribute('disabled', 'disabled');
|
||||
|
||||
this.createTargetButton.classList.add('disabled');
|
||||
this.createTargetButton.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
|
||||
enable() {
|
||||
this.createMergeRequestButton.classList.remove('disabled');
|
||||
this.createMergeRequestButton.removeAttribute('disabled');
|
||||
|
||||
this.createTargetButton.classList.remove('disabled');
|
||||
this.createTargetButton.removeAttribute('disabled');
|
||||
|
||||
this.dropdownToggle.classList.remove('disabled');
|
||||
this.dropdownToggle.removeAttribute('disabled');
|
||||
}
|
||||
|
||||
static findByValue(objects, ref, returnFirstMatch = false) {
|
||||
if (!objects || !objects.length) return false;
|
||||
if (objects.indexOf(ref) > -1) return ref;
|
||||
if (returnFirstMatch) return objects.find(item => new RegExp(`^${ref}`).test(item));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
getDroplabConfig() {
|
||||
return {
|
||||
addActiveClassToDropdownButton: true,
|
||||
InputSetter: [
|
||||
{
|
||||
input: this.createMergeRequestButton,
|
||||
valueAttribute: 'data-value',
|
||||
inputAttribute: 'data-action',
|
||||
},
|
||||
{
|
||||
input: this.createMergeRequestButton,
|
||||
valueAttribute: 'data-text',
|
||||
},
|
||||
{
|
||||
input: this.createTargetButton,
|
||||
valueAttribute: 'data-value',
|
||||
inputAttribute: 'data-action',
|
||||
},
|
||||
{
|
||||
input: this.createTargetButton,
|
||||
valueAttribute: 'data-text',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
static getInputSelectedText(input) {
|
||||
const start = input.selectionStart;
|
||||
const end = input.selectionEnd;
|
||||
|
||||
return input.value.substr(start, end - start);
|
||||
}
|
||||
|
||||
getRef(ref, target = 'all') {
|
||||
if (!ref) return false;
|
||||
|
||||
return $.ajax({
|
||||
method: 'POST',
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
url: this.createBranchPath,
|
||||
beforeSend: () => (this.isCreatingBranch = true),
|
||||
url: this.refsPath + ref,
|
||||
beforeSend: () => {
|
||||
this.isGettingRef = true;
|
||||
},
|
||||
})
|
||||
.always(() => {
|
||||
this.isGettingRef = false;
|
||||
})
|
||||
.done((data) => {
|
||||
this.branchCreated = true;
|
||||
window.location.href = data.url;
|
||||
const branches = data[Object.keys(data)[0]];
|
||||
const tags = data[Object.keys(data)[1]];
|
||||
let result;
|
||||
|
||||
if (target === 'branch') {
|
||||
result = CreateMergeRequestDropdown.findByValue(branches, ref);
|
||||
} else {
|
||||
result = CreateMergeRequestDropdown.findByValue(branches, ref, true) ||
|
||||
CreateMergeRequestDropdown.findByValue(tags, ref, true);
|
||||
this.suggestedRef = result;
|
||||
}
|
||||
|
||||
return this.updateInputState(target, ref, result);
|
||||
})
|
||||
.fail(() => new Flash('Failed to create a branch for this issue. Please try again.'));
|
||||
.fail(() => {
|
||||
this.unavailable();
|
||||
this.disable();
|
||||
new Flash('Failed to get ref.');
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
getTargetData(target) {
|
||||
return {
|
||||
input: this[`${target}Input`],
|
||||
message: this[`${target}Message`],
|
||||
};
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.wrapperEl.classList.add('hide');
|
||||
}
|
||||
|
||||
init() {
|
||||
this.checkAbilityToCreateBranch();
|
||||
}
|
||||
|
||||
initDroplab() {
|
||||
this.droplab = new DropLab();
|
||||
|
||||
this.droplab.init(
|
||||
this.dropdownToggle,
|
||||
this.dropdownList,
|
||||
[InputSetter],
|
||||
this.getDroplabConfig(),
|
||||
);
|
||||
}
|
||||
|
||||
inputsAreValid() {
|
||||
return this.branchIsValid && this.refIsValid;
|
||||
}
|
||||
|
||||
isBusy() {
|
||||
return this.isCreatingMergeRequest ||
|
||||
this.mergeRequestCreated ||
|
||||
this.isCreatingBranch ||
|
||||
this.branchCreated ||
|
||||
this.isGettingRef;
|
||||
}
|
||||
|
||||
onChangeInput(event) {
|
||||
let target;
|
||||
let value;
|
||||
|
||||
if (event.srcElement === this.branchInput) {
|
||||
target = 'branch';
|
||||
value = this.branchInput.value;
|
||||
} else if (event.srcElement === this.refInput) {
|
||||
target = 'ref';
|
||||
value = event.srcElement.value.slice(0, event.srcElement.selectionStart) +
|
||||
event.srcElement.value.slice(event.srcElement.selectionEnd);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isGettingRef) return false;
|
||||
|
||||
// `ENTER` key submits the data.
|
||||
if (event.keyCode === 13 && this.inputsAreValid()) {
|
||||
event.preventDefault();
|
||||
return this.createMergeRequestButton.click();
|
||||
}
|
||||
|
||||
// If the input is empty, use the original value generated by the backend.
|
||||
if (!value) {
|
||||
this.createBranchPath = this.wrapperEl.dataset.createBranchPath;
|
||||
this.createMrPath = this.wrapperEl.dataset.createMrPath;
|
||||
|
||||
if (target === 'branch') {
|
||||
this.branchIsValid = true;
|
||||
} else {
|
||||
this.refIsValid = true;
|
||||
}
|
||||
|
||||
this.enable();
|
||||
this.showAvailableMessage(target);
|
||||
return true;
|
||||
}
|
||||
|
||||
this.showCheckingMessage(target);
|
||||
this.refDebounce(value, target);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
onClickCreateMergeRequestButton(event) {
|
||||
let xhr = null;
|
||||
event.preventDefault();
|
||||
|
||||
if (this.isBusy()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.dataset.action === CREATE_MERGE_REQUEST) {
|
||||
xhr = this.createMergeRequest();
|
||||
} else if (event.target.dataset.action === CREATE_BRANCH) {
|
||||
xhr = this.createBranch();
|
||||
}
|
||||
|
||||
xhr.fail(() => {
|
||||
this.isCreatingMergeRequest = false;
|
||||
this.isCreatingBranch = false;
|
||||
});
|
||||
|
||||
xhr.always(() => this.enable());
|
||||
|
||||
this.disable();
|
||||
}
|
||||
|
||||
onClickSetFocusOnBranchNameInput() {
|
||||
this.branchInput.focus();
|
||||
}
|
||||
|
||||
// `TAB` autocompletes the source.
|
||||
static processTab(event) {
|
||||
if (event.keyCode !== 9 || this.isGettingRef) return;
|
||||
|
||||
const selectedText = CreateMergeRequestDropdown.getInputSelectedText(this.refInput);
|
||||
|
||||
// if nothing selected, we don't need to autocomplete anything. Do the default TAB action.
|
||||
// If a user manually selected text, don't autocomplete anything. Do the default TAB action.
|
||||
if (!selectedText || this.refInput.dataset.value === this.suggestedRef) return;
|
||||
|
||||
event.preventDefault();
|
||||
window.getSelection().removeAllRanges();
|
||||
}
|
||||
|
||||
removeMessage(target) {
|
||||
const { input, message } = this.getTargetData(target);
|
||||
const inputClasses = ['gl-field-error-outline', 'gl-field-success-outline'];
|
||||
const messageClasses = ['gl-field-hint', 'gl-field-error-message', 'gl-field-success-message'];
|
||||
|
||||
inputClasses.forEach(cssClass => input.classList.remove(cssClass));
|
||||
messageClasses.forEach(cssClass => message.classList.remove(cssClass));
|
||||
message.style.display = 'none';
|
||||
}
|
||||
|
||||
setUnavailableButtonState(isLoading = true) {
|
||||
if (isLoading) {
|
||||
this.unavailableButtonArrow.classList.add('fa-spin');
|
||||
this.unavailableButtonArrow.classList.add('fa-spinner');
|
||||
this.unavailableButtonArrow.classList.remove('fa-exclamation-triangle');
|
||||
this.unavailableButtonText.textContent = __('Checking branch availability...');
|
||||
} else {
|
||||
this.unavailableButtonArrow.classList.remove('fa-spin');
|
||||
this.unavailableButtonArrow.classList.remove('fa-spinner');
|
||||
this.unavailableButtonArrow.classList.add('fa-exclamation-triangle');
|
||||
this.unavailableButtonText.textContent = __('New branch unavailable');
|
||||
}
|
||||
}
|
||||
|
||||
showAvailableMessage(target) {
|
||||
const { input, message } = this.getTargetData(target);
|
||||
const text = target === 'branch' ? __('Branch name') : __('Source');
|
||||
|
||||
this.removeMessage(target);
|
||||
input.classList.add('gl-field-success-outline');
|
||||
message.classList.add('gl-field-success-message');
|
||||
message.textContent = sprintf(__('%{text} is available'), { text });
|
||||
message.style.display = 'inline-block';
|
||||
}
|
||||
|
||||
showCheckingMessage(target) {
|
||||
const { message } = this.getTargetData(target);
|
||||
const text = target === 'branch' ? __('branch name') : __('source');
|
||||
|
||||
this.removeMessage(target);
|
||||
message.classList.add('gl-field-hint');
|
||||
message.textContent = sprintf(__('Checking %{text} availability…'), { text });
|
||||
message.style.display = 'inline-block';
|
||||
}
|
||||
|
||||
showNotAvailableMessage(target) {
|
||||
const { input, message } = this.getTargetData(target);
|
||||
const text = target === 'branch' ? __('Branch is already taken') : __('Source is not available');
|
||||
|
||||
this.removeMessage(target);
|
||||
input.classList.add('gl-field-error-outline');
|
||||
message.classList.add('gl-field-error-message');
|
||||
message.textContent = text;
|
||||
message.style.display = 'inline-block';
|
||||
}
|
||||
|
||||
unavailable() {
|
||||
this.availableButton.classList.add('hide');
|
||||
this.unavailableButton.classList.remove('hide');
|
||||
}
|
||||
|
||||
updateInputState(target, ref, result) {
|
||||
// target - 'branch' or 'ref' - which the input field we are searching a ref for.
|
||||
// ref - string - what a user typed.
|
||||
// result - string - what has been found on backend.
|
||||
|
||||
const pathReplacement = `$1${ref}`;
|
||||
|
||||
// If a found branch equals exact the same text a user typed,
|
||||
// that means a new branch cannot be created as it already exists.
|
||||
if (ref === result) {
|
||||
if (target === 'branch') {
|
||||
this.branchIsValid = false;
|
||||
this.showNotAvailableMessage('branch');
|
||||
} else {
|
||||
this.refIsValid = true;
|
||||
this.refInput.dataset.value = ref;
|
||||
this.showAvailableMessage('ref');
|
||||
this.createBranchPath = this.createBranchPath.replace(this.regexps.ref.createBranchPath,
|
||||
pathReplacement);
|
||||
this.createMrPath = this.createMrPath.replace(this.regexps.ref.createMrPath,
|
||||
pathReplacement);
|
||||
}
|
||||
} else if (target === 'branch') {
|
||||
this.branchIsValid = true;
|
||||
this.showAvailableMessage('branch');
|
||||
this.createBranchPath = this.createBranchPath.replace(this.regexps.branch.createBranchPath,
|
||||
pathReplacement);
|
||||
this.createMrPath = this.createMrPath.replace(this.regexps.branch.createMrPath,
|
||||
pathReplacement);
|
||||
} else {
|
||||
this.refIsValid = false;
|
||||
this.refInput.dataset.value = ref;
|
||||
this.disableCreateAction();
|
||||
this.showNotAvailableMessage('ref');
|
||||
|
||||
// Show ref hint.
|
||||
if (result) {
|
||||
this.refInput.value = result;
|
||||
this.refInput.setSelectionRange(ref.length, result.length);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.inputsAreValid()) {
|
||||
this.enable();
|
||||
} else {
|
||||
this.disableCreateAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -383,6 +383,7 @@ import ProjectVariables from './project_variables';
|
|||
projectImport();
|
||||
break;
|
||||
case 'projects:pipelines:new':
|
||||
case 'projects:pipelines:create':
|
||||
new NewBranchForm($('.js-new-pipeline-form'));
|
||||
break;
|
||||
case 'projects:pipelines:builds':
|
||||
|
|
@ -521,6 +522,13 @@ import ProjectVariables from './project_variables';
|
|||
case 'projects:settings:ci_cd:show':
|
||||
// Initialize expandable settings panels
|
||||
initSettingsPanels();
|
||||
|
||||
import(/* webpackChunkName: "ci-cd-settings" */ './projects/ci_cd_settings_bundle')
|
||||
.then(ciCdSettings => ciCdSettings.default())
|
||||
.catch((err) => {
|
||||
Flash(s__('ProjectSettings|Problem setting up the CI/CD settings JavaScript'));
|
||||
throw err;
|
||||
});
|
||||
case 'groups:settings:ci_cd:show':
|
||||
new ProjectVariables();
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ const DATA_DROPDOWN = 'data-dropdown';
|
|||
const SELECTED_CLASS = 'droplab-item-selected';
|
||||
const ACTIVE_CLASS = 'droplab-item-active';
|
||||
const IGNORE_CLASS = 'droplab-item-ignore';
|
||||
const IGNORE_HIDING_CLASS = 'droplab-item-ignore-hiding';
|
||||
// Matches `{{anything}}` and `{{ everything }}`.
|
||||
const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g;
|
||||
|
||||
|
|
@ -13,4 +14,5 @@ export {
|
|||
ACTIVE_CLASS,
|
||||
TEMPLATE_REGEX,
|
||||
IGNORE_CLASS,
|
||||
IGNORE_HIDING_CLASS,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
import utils from './utils';
|
||||
import { SELECTED_CLASS, IGNORE_CLASS } from './constants';
|
||||
import { SELECTED_CLASS, IGNORE_CLASS, IGNORE_HIDING_CLASS } from './constants';
|
||||
|
||||
class DropDown {
|
||||
constructor(list) {
|
||||
constructor(list, config = {}) {
|
||||
this.currentIndex = 0;
|
||||
this.hidden = true;
|
||||
this.list = typeof list === 'string' ? document.querySelector(list) : list;
|
||||
this.items = [];
|
||||
|
||||
this.eventWrapper = {};
|
||||
|
||||
if (config.addActiveClassToDropdownButton) {
|
||||
this.dropdownToggle = this.list.parentNode.querySelector('.js-dropdown-toggle');
|
||||
}
|
||||
|
||||
this.getItems();
|
||||
this.initTemplateString();
|
||||
this.addEvents();
|
||||
|
|
@ -42,7 +45,7 @@ class DropDown {
|
|||
this.addSelectedClass(selected);
|
||||
|
||||
e.preventDefault();
|
||||
this.hide();
|
||||
if (!e.target.classList.contains(IGNORE_HIDING_CLASS)) this.hide();
|
||||
|
||||
const listEvent = new CustomEvent('click.dl', {
|
||||
detail: {
|
||||
|
|
@ -67,7 +70,20 @@ class DropDown {
|
|||
|
||||
addEvents() {
|
||||
this.eventWrapper.clickEvent = this.clickEvent.bind(this);
|
||||
this.eventWrapper.closeDropdown = this.closeDropdown.bind(this);
|
||||
|
||||
this.list.addEventListener('click', this.eventWrapper.clickEvent);
|
||||
this.list.addEventListener('keyup', this.eventWrapper.closeDropdown);
|
||||
}
|
||||
|
||||
closeDropdown(event) {
|
||||
// `ESC` key closes the dropdown.
|
||||
if (event.keyCode === 27) {
|
||||
event.preventDefault();
|
||||
return this.toggle();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
setData(data) {
|
||||
|
|
@ -110,6 +126,8 @@ class DropDown {
|
|||
this.list.style.display = 'block';
|
||||
this.currentIndex = 0;
|
||||
this.hidden = false;
|
||||
|
||||
if (this.dropdownToggle) this.dropdownToggle.classList.add('active');
|
||||
}
|
||||
|
||||
hide() {
|
||||
|
|
@ -117,6 +135,8 @@ class DropDown {
|
|||
this.list.style.display = 'none';
|
||||
this.currentIndex = 0;
|
||||
this.hidden = true;
|
||||
|
||||
if (this.dropdownToggle) this.dropdownToggle.classList.remove('active');
|
||||
}
|
||||
|
||||
toggle() {
|
||||
|
|
@ -128,6 +148,7 @@ class DropDown {
|
|||
destroy() {
|
||||
this.hide();
|
||||
this.list.removeEventListener('click', this.eventWrapper.clickEvent);
|
||||
this.list.removeEventListener('keyup', this.eventWrapper.closeDropdown);
|
||||
}
|
||||
|
||||
static setImagesSrc(template) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import DropDown from './drop_down';
|
|||
class Hook {
|
||||
constructor(trigger, list, plugins, config) {
|
||||
this.trigger = trigger;
|
||||
this.list = new DropDown(list);
|
||||
this.list = new DropDown(list, config);
|
||||
this.type = 'Hook';
|
||||
this.event = 'click';
|
||||
this.plugins = plugins || [];
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ const createFlashEl = (message, type, isInContentWrapper = false) => `
|
|||
`;
|
||||
|
||||
const removeFlashClickListener = (flashEl, fadeTransition) => {
|
||||
flashEl.parentNode.addEventListener('click', () => hideFlash(flashEl, fadeTransition));
|
||||
flashEl.addEventListener('click', () => hideFlash(flashEl, fadeTransition));
|
||||
};
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ export default {
|
|||
required: true,
|
||||
type: String,
|
||||
},
|
||||
updateEndpoint: {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
canUpdate: {
|
||||
required: true,
|
||||
type: Boolean,
|
||||
|
|
@ -262,6 +266,8 @@ export default {
|
|||
:description-text="state.descriptionText"
|
||||
:updated-at="state.updatedAt"
|
||||
:task-status="state.taskStatus"
|
||||
:issuable-type="issuableType"
|
||||
:update-url="updateEndpoint"
|
||||
/>
|
||||
<edited-component
|
||||
v-if="hasUpdated"
|
||||
|
|
|
|||
|
|
@ -22,6 +22,16 @@
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
issuableType: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'issue',
|
||||
},
|
||||
updateUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -48,7 +58,7 @@
|
|||
if (this.canUpdate) {
|
||||
// eslint-disable-next-line no-new
|
||||
new TaskList({
|
||||
dataType: 'issue',
|
||||
dataType: this.issuableType,
|
||||
fieldName: 'description',
|
||||
selector: '.detail-page-description',
|
||||
});
|
||||
|
|
@ -95,7 +105,9 @@
|
|||
<textarea
|
||||
class="hidden js-task-list-field"
|
||||
v-if="descriptionText"
|
||||
v-model="descriptionText">
|
||||
v-model="descriptionText"
|
||||
:data-update-url="updateUrl"
|
||||
>
|
||||
</textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
v-tooltip
|
||||
v-if="showInlineEditButton && canUpdate"
|
||||
type="button"
|
||||
class="btn-blank btn-edit note-action-button"
|
||||
class="btn btn-default btn-edit btn-svg"
|
||||
v-html="pencilIcon"
|
||||
title="Edit title and description"
|
||||
data-placement="bottom"
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ export const insertText = (target, text) => {
|
|||
target.selectionStart = target.selectionEnd = selectionStart + insertedText.length;
|
||||
|
||||
// Trigger autosave
|
||||
$(target).trigger('input');
|
||||
target.dispatchEvent(new Event('input'));
|
||||
|
||||
// Trigger autosize
|
||||
const event = document.createEvent('Event');
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ import './commits';
|
|||
import './compare';
|
||||
import './compare_autocomplete';
|
||||
import './confirm_danger_modal';
|
||||
import './copy_to_clipboard';
|
||||
import Flash, { removeFlashClickListener } from './flash';
|
||||
import './gl_dropdown';
|
||||
import './gl_field_error';
|
||||
|
|
@ -301,6 +300,8 @@ $(function () {
|
|||
const flashContainer = document.querySelector('.flash-container');
|
||||
|
||||
if (flashContainer && flashContainer.children.length) {
|
||||
removeFlashClickListener(flashContainer.children[0]);
|
||||
flashContainer.querySelectorAll('.flash-alert, .flash-notice, .flash-success').forEach((flashEl) => {
|
||||
removeFlashClickListener(flashEl);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,13 +17,14 @@ export default class Project {
|
|||
$('a', $cloneOptions).on('click', (e) => {
|
||||
const $this = $(e.currentTarget);
|
||||
const url = $this.attr('href');
|
||||
const activeText = $this.find('.dropdown-menu-inner-title').text();
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
$('.is-active', $cloneOptions).not($this).removeClass('is-active');
|
||||
$this.toggleClass('is-active');
|
||||
$projectCloneField.val(url);
|
||||
$cloneBtnText.text($this.text());
|
||||
$cloneBtnText.text(activeText);
|
||||
|
||||
return $('.clone').text(url);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
function updateAutoDevopsRadios(radioWrappers) {
|
||||
radioWrappers.forEach((radioWrapper) => {
|
||||
const radio = radioWrapper.querySelector('.js-auto-devops-enable-radio');
|
||||
const runPipelineCheckboxWrapper = radioWrapper.querySelector('.js-run-auto-devops-pipeline-checkbox-wrapper');
|
||||
const runPipelineCheckbox = radioWrapper.querySelector('.js-run-auto-devops-pipeline-checkbox');
|
||||
|
||||
if (runPipelineCheckbox) {
|
||||
runPipelineCheckbox.checked = radio.checked;
|
||||
runPipelineCheckboxWrapper.classList.toggle('hide', !radio.checked);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default function initCiCdSettings() {
|
||||
const radioWrappers = document.querySelectorAll('.js-auto-devops-enable-radio-wrapper');
|
||||
radioWrappers.forEach(radioWrapper =>
|
||||
radioWrapper.addEventListener('change', () => updateAutoDevopsRadios(radioWrappers)),
|
||||
);
|
||||
}
|
||||
|
|
@ -48,6 +48,27 @@ export default {
|
|||
}
|
||||
return this.projectName;
|
||||
},
|
||||
/**
|
||||
* Smartly truncates project namespace by doing two things;
|
||||
* 1. Only include Group names in path by removing project name
|
||||
* 2. Only include first and last group names in the path
|
||||
* when namespace has more than 2 groups present
|
||||
*
|
||||
* First part (removal of project name from namespace) can be
|
||||
* done from backend but doing so involves migration of
|
||||
* existing project namespaces which is not wise thing to do.
|
||||
*/
|
||||
truncatedNamespace() {
|
||||
const namespaceArr = this.namespace.split(' / ');
|
||||
namespaceArr.splice(-1, 1);
|
||||
let namespace = namespaceArr.join(' / ');
|
||||
|
||||
if (namespaceArr.length > 2) {
|
||||
namespace = `${namespaceArr[0]} / ... / ${namespaceArr.pop()}`;
|
||||
}
|
||||
|
||||
return namespace;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -87,9 +108,7 @@ export default {
|
|||
<div
|
||||
class="project-namespace"
|
||||
:title="namespace"
|
||||
>
|
||||
{{namespace}}
|
||||
</div>
|
||||
>{{truncatedNamespace}}</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
<script>
|
||||
import icon from '../../../vue_shared/components/icon.vue';
|
||||
import listItem from './list_item.vue';
|
||||
import listCollapsed from './list_collapsed.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
listItem,
|
||||
listCollapsed,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fileList: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleCollapsed() {
|
||||
this.$emit('toggleCollapsed');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="multi-file-commit-panel-section">
|
||||
<header
|
||||
class="multi-file-commit-panel-header"
|
||||
:class="{
|
||||
'is-collapsed': collapsed,
|
||||
}"
|
||||
>
|
||||
<icon
|
||||
name="list-bulleted"
|
||||
:size="18"
|
||||
css-classes="append-right-default"
|
||||
/>
|
||||
<template v-if="!collapsed">
|
||||
{{ title }}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-transparent multi-file-commit-panel-collapse-btn"
|
||||
@click="toggleCollapsed"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-angle-double-right"
|
||||
>
|
||||
</i>
|
||||
</button>
|
||||
</template>
|
||||
</header>
|
||||
<div class="multi-file-commit-list">
|
||||
<list-collapsed
|
||||
v-if="collapsed"
|
||||
/>
|
||||
<template v-else>
|
||||
<ul
|
||||
v-if="fileList.length"
|
||||
class="list-unstyled append-bottom-0"
|
||||
>
|
||||
<li
|
||||
v-for="file in fileList"
|
||||
:key="file.key"
|
||||
>
|
||||
<list-item
|
||||
:file="file"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
v-else
|
||||
class="help-block prepend-top-0"
|
||||
>
|
||||
No changes
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import icon from '../../../vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'addedFiles',
|
||||
'modifiedFiles',
|
||||
]),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="multi-file-commit-list-collapsed text-center"
|
||||
>
|
||||
<icon
|
||||
name="file-addition"
|
||||
:size="18"
|
||||
css-classes="multi-file-addition append-bottom-10"
|
||||
/>
|
||||
{{ addedFiles.length }}
|
||||
<icon
|
||||
name="file-modified"
|
||||
:size="18"
|
||||
css-classes="multi-file-modified prepend-top-10 append-bottom-10"
|
||||
/>
|
||||
{{ modifiedFiles.length }}
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<script>
|
||||
import icon from '../../../vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
icon,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
iconName() {
|
||||
return this.file.tempFile ? 'file-addition' : 'file-modified';
|
||||
},
|
||||
iconClass() {
|
||||
return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="multi-file-commit-list-item">
|
||||
<icon
|
||||
:name="iconName"
|
||||
:size="16"
|
||||
:css-classes="iconClass"
|
||||
/>
|
||||
<span class="multi-file-commit-list-path">
|
||||
{{ file.path }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -40,20 +40,24 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="repository-view">
|
||||
<div class="tree-content-holder" :class="{'tree-content-holder-mini' : isCollapsed}">
|
||||
<repo-sidebar/>
|
||||
<div
|
||||
v-if="isCollapsed"
|
||||
class="panel-right"
|
||||
>
|
||||
<repo-tabs/>
|
||||
<component
|
||||
:is="currentBlobView"
|
||||
/>
|
||||
<repo-file-buttons/>
|
||||
</div>
|
||||
<div
|
||||
class="multi-file"
|
||||
:class="{
|
||||
'is-collapsed': isCollapsed
|
||||
}"
|
||||
>
|
||||
<repo-sidebar/>
|
||||
<div
|
||||
v-if="isCollapsed"
|
||||
class="multi-file-edit-pane"
|
||||
>
|
||||
<repo-tabs />
|
||||
<component
|
||||
class="multi-file-edit-pane-content"
|
||||
:is="currentBlobView"
|
||||
/>
|
||||
<repo-file-buttons />
|
||||
</div>
|
||||
<repo-commit-section v-if="changedFiles.length" />
|
||||
<repo-commit-section />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
<script>
|
||||
import { mapGetters, mapState, mapActions } from 'vuex';
|
||||
import tooltip from '../../vue_shared/directives/tooltip';
|
||||
import icon from '../../vue_shared/components/icon.vue';
|
||||
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
|
||||
import { n__ } from '../../locale';
|
||||
import commitFilesList from './commit_sidebar/list.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PopupDialog,
|
||||
icon,
|
||||
commitFilesList,
|
||||
},
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -13,6 +20,7 @@ export default {
|
|||
submitCommitsLoading: false,
|
||||
startNewMR: false,
|
||||
commitMessage: '',
|
||||
collapsed: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -23,10 +31,10 @@ export default {
|
|||
'changedFiles',
|
||||
]),
|
||||
commitButtonDisabled() {
|
||||
return !this.commitMessage || this.submitCommitsLoading;
|
||||
return this.commitMessage === '' || this.submitCommitsLoading || !this.changedFiles.length;
|
||||
},
|
||||
commitButtonText() {
|
||||
return n__('Commit %d file', 'Commit %d files', this.changedFiles.length);
|
||||
commitMessageCount() {
|
||||
return this.commitMessage.length;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -77,12 +85,20 @@ export default {
|
|||
this.submitCommitsLoading = false;
|
||||
});
|
||||
},
|
||||
toggleCollapsed() {
|
||||
this.collapsed = !this.collapsed;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="commit-area">
|
||||
<div
|
||||
class="multi-file-commit-panel"
|
||||
:class="{
|
||||
'is-collapsed': collapsed,
|
||||
}"
|
||||
>
|
||||
<popup-dialog
|
||||
v-if="showNewBranchDialog"
|
||||
:primary-button-label="__('Create new branch')"
|
||||
|
|
@ -92,78 +108,71 @@ export default {
|
|||
@toggle="showNewBranchDialog = false"
|
||||
@submit="makeCommit(true)"
|
||||
/>
|
||||
<button
|
||||
v-if="collapsed"
|
||||
type="button"
|
||||
class="btn btn-transparent multi-file-commit-panel-collapse-btn is-collapsed prepend-top-10 append-bottom-10"
|
||||
@click="toggleCollapsed"
|
||||
>
|
||||
<i
|
||||
aria-hidden="true"
|
||||
class="fa fa-angle-double-left"
|
||||
>
|
||||
</i>
|
||||
</button>
|
||||
<commit-files-list
|
||||
title="Staged"
|
||||
:file-list="changedFiles"
|
||||
:collapsed="collapsed"
|
||||
@toggleCollapsed="toggleCollapsed"
|
||||
/>
|
||||
<form
|
||||
class="form-horizontal"
|
||||
@submit.prevent="tryCommit()">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="col-md-4 control-label staged-files">
|
||||
Staged files ({{changedFiles.length}})
|
||||
</label>
|
||||
<div class="col-md-6">
|
||||
<ul class="list-unstyled changed-files">
|
||||
<li
|
||||
v-for="(file, index) in changedFiles"
|
||||
:key="index">
|
||||
<span class="help-block">
|
||||
{{ file.path }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
class="form-horizontal multi-file-commit-form"
|
||||
@submit.prevent="tryCommit"
|
||||
v-if="!collapsed"
|
||||
>
|
||||
<div class="multi-file-commit-fieldset">
|
||||
<textarea
|
||||
class="form-control multi-file-commit-message"
|
||||
name="commit-message"
|
||||
v-model="commitMessage"
|
||||
placeholder="Commit message"
|
||||
>
|
||||
</textarea>
|
||||
</div>
|
||||
<div class="multi-file-commit-fieldset">
|
||||
<label
|
||||
v-tooltip
|
||||
title="Create a new merge request with these changes"
|
||||
data-container="body"
|
||||
data-placement="top"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
v-model="startNewMR"
|
||||
/>
|
||||
Merge Request
|
||||
</label>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="commitButtonDisabled"
|
||||
class="btn btn-default btn-sm append-right-10 prepend-left-10"
|
||||
>
|
||||
<i
|
||||
v-if="submitCommitsLoading"
|
||||
class="js-commit-loading-icon fa fa-spinner fa-spin"
|
||||
aria-hidden="true"
|
||||
aria-label="loading"
|
||||
>
|
||||
</i>
|
||||
Commit
|
||||
</button>
|
||||
<div
|
||||
class="multi-file-commit-message-count"
|
||||
>
|
||||
{{ commitMessageCount }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label
|
||||
class="col-md-4 control-label"
|
||||
for="commit-message">
|
||||
Commit message
|
||||
</label>
|
||||
<div class="col-md-6">
|
||||
<textarea
|
||||
id="commit-message"
|
||||
class="form-control"
|
||||
name="commit-message"
|
||||
v-model="commitMessage">
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group target-branch">
|
||||
<label
|
||||
class="col-md-4 control-label"
|
||||
for="target-branch">
|
||||
Target branch
|
||||
</label>
|
||||
<div class="col-md-6">
|
||||
<span class="help-block">
|
||||
{{currentBranch}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-offset-4 col-md-6">
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="commitButtonDisabled"
|
||||
class="btn btn-success">
|
||||
<i
|
||||
v-if="submitCommitsLoading"
|
||||
class="js-commit-loading-icon fa fa-spinner fa-spin"
|
||||
aria-hidden="true"
|
||||
aria-label="loading">
|
||||
</i>
|
||||
<span class="commit-summary">
|
||||
{{ commitButtonText }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-offset-4 col-md-6">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" v-model="startNewMR">
|
||||
<span>Start a <strong>new merge request</strong> with these changes</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@
|
|||
class="file"
|
||||
@click.prevent="clickedTreeRow(file)">
|
||||
<td
|
||||
class="multi-file-table-col-name"
|
||||
class="multi-file-table-name"
|
||||
:colspan="submoduleColSpan"
|
||||
>
|
||||
<i
|
||||
|
|
@ -85,12 +85,11 @@
|
|||
</td>
|
||||
|
||||
<template v-if="!isCollapsed && !isSubmodule">
|
||||
<td class="hidden-sm hidden-xs">
|
||||
<td class="multi-file-table-col-commit-message hidden-sm hidden-xs">
|
||||
<a
|
||||
v-if="file.lastCommit.message"
|
||||
@click.stop
|
||||
:href="file.lastCommit.url"
|
||||
class="commit-message"
|
||||
>
|
||||
{{ file.lastCommit.message }}
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -22,12 +22,12 @@ export default {
|
|||
<template>
|
||||
<div
|
||||
v-if="showButtons"
|
||||
class="repo-file-buttons"
|
||||
class="multi-file-editor-btn-group"
|
||||
>
|
||||
<a
|
||||
:href="activeFile.rawPath"
|
||||
target="_blank"
|
||||
class="btn btn-default raw"
|
||||
class="btn btn-default btn-sm raw"
|
||||
rel="noopener noreferrer">
|
||||
{{ rawDownloadButtonLabel }}
|
||||
</a>
|
||||
|
|
@ -38,17 +38,17 @@ export default {
|
|||
aria-label="File actions">
|
||||
<a
|
||||
:href="activeFile.blamePath"
|
||||
class="btn btn-default blame">
|
||||
class="btn btn-default btn-sm blame">
|
||||
Blame
|
||||
</a>
|
||||
<a
|
||||
:href="activeFile.commitsPath"
|
||||
class="btn btn-default history">
|
||||
class="btn btn-default btn-sm history">
|
||||
History
|
||||
</a>
|
||||
<a
|
||||
:href="activeFile.permalink"
|
||||
class="btn btn-default permalink">
|
||||
class="btn btn-default btn-sm permalink">
|
||||
Permalink
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -32,10 +32,12 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="blob-viewer-container">
|
||||
<div>
|
||||
<div
|
||||
v-if="!activeFile.renderError"
|
||||
v-html="activeFile.html">
|
||||
v-html="activeFile.html"
|
||||
class="multi-file-preview-holder"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="activeFile.tempFile"
|
||||
|
|
|
|||
|
|
@ -44,20 +44,16 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div id="sidebar" :class="{'sidebar-mini' : isCollapsed}">
|
||||
<div class="ide-file-list">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
v-if="isCollapsed"
|
||||
class="repo-file-options title"
|
||||
>
|
||||
<strong class="clgray">
|
||||
{{ projectName }}
|
||||
</strong>
|
||||
</th>
|
||||
<template v-else>
|
||||
<th class="name multi-file-table-col-name">
|
||||
<th class="name multi-file-table-name">
|
||||
Name
|
||||
</th>
|
||||
<th class="hidden-sm hidden-xs last-commit">
|
||||
|
|
@ -79,7 +75,7 @@ export default {
|
|||
:key="n"
|
||||
/>
|
||||
<repo-file
|
||||
v-for="(file, index) in treeList"
|
||||
v-for="file in treeList"
|
||||
:key="file.key"
|
||||
:file="file"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -36,27 +36,32 @@ export default {
|
|||
|
||||
<template>
|
||||
<li
|
||||
:class="{ active : tab.active }"
|
||||
@click="setFileActive(tab)"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="close-btn"
|
||||
class="multi-file-tab-close"
|
||||
@click.stop.prevent="closeFile({ file: tab })"
|
||||
:aria-label="closeLabel">
|
||||
:aria-label="closeLabel"
|
||||
:class="{
|
||||
'modified': tab.changed,
|
||||
}"
|
||||
:disabled="tab.changed"
|
||||
>
|
||||
<i
|
||||
class="fa"
|
||||
:class="changedClass"
|
||||
aria-hidden="true">
|
||||
aria-hidden="true"
|
||||
>
|
||||
</i>
|
||||
</button>
|
||||
|
||||
<a
|
||||
href="#"
|
||||
class="repo-tab"
|
||||
<div
|
||||
class="multi-file-tab"
|
||||
:class="{active : tab.active }"
|
||||
:title="tab.url"
|
||||
@click.prevent.stop="setFileActive(tab)">
|
||||
{{tab.name}}
|
||||
</a>
|
||||
>
|
||||
{{ tab.name }}
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -16,14 +16,12 @@
|
|||
|
||||
<template>
|
||||
<ul
|
||||
id="tabs"
|
||||
class="list-unstyled"
|
||||
class="multi-file-tabs list-unstyled append-bottom-0"
|
||||
>
|
||||
<repo-tab
|
||||
v-for="tab in openFiles"
|
||||
:key="tab.id"
|
||||
:tab="tab"
|
||||
/>
|
||||
<li class="tabs-divider" />
|
||||
</ul>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -34,3 +34,7 @@ export const canEditFile = (state) => {
|
|||
openedFiles.length &&
|
||||
(currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
|
||||
};
|
||||
|
||||
export const addedFiles = state => changedFiles(state).filter(f => f.tempFile);
|
||||
|
||||
export const modifiedFiles = state => changedFiles(state).filter(f => !f.tempFile);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@
|
|||
/>
|
||||
|
||||
*/
|
||||
// only allow classes in images.scss e.g. s12
|
||||
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
|
||||
|
||||
export default {
|
||||
props: {
|
||||
name: {
|
||||
|
|
@ -22,7 +25,10 @@
|
|||
size: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
default: 16,
|
||||
validator(value) {
|
||||
return validSizes.includes(value);
|
||||
},
|
||||
},
|
||||
|
||||
cssClasses: {
|
||||
|
|
@ -42,10 +48,11 @@
|
|||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<svg
|
||||
:class="[iconSizeClass, cssClasses]">
|
||||
<use
|
||||
<use
|
||||
v-bind="{'xlink:href':spriteHref}"/>
|
||||
</svg>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@
|
|||
@include transition(border-color);
|
||||
}
|
||||
|
||||
.note-action-button .link-highlight,
|
||||
.note-action-button,
|
||||
.toolbar-btn,
|
||||
.dropdown-toggle-caret {
|
||||
@include transition(color);
|
||||
|
|
|
|||
|
|
@ -88,17 +88,6 @@
|
|||
border-color: $border-dark;
|
||||
color: $color;
|
||||
}
|
||||
|
||||
svg {
|
||||
|
||||
path {
|
||||
fill: $color;
|
||||
}
|
||||
|
||||
use {
|
||||
stroke: $color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin btn-green {
|
||||
|
|
@ -142,6 +131,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
@mixin btn-svg {
|
||||
height: $gl-padding;
|
||||
width: $gl-padding;
|
||||
top: 0;
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
.btn {
|
||||
@include btn-default;
|
||||
@include btn-white;
|
||||
|
|
@ -440,3 +436,7 @@
|
|||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-svg svg {
|
||||
@include btn-svg;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,43 @@
|
|||
.cgray { color: $common-gray; }
|
||||
.clgray { color: $common-gray-light; }
|
||||
.cred { color: $common-red; }
|
||||
svg.cred { fill: $common-red; }
|
||||
.cgreen { color: $common-green; }
|
||||
svg.cgreen { fill: $common-green; }
|
||||
.cdark { color: $common-gray-dark; }
|
||||
|
||||
.text-plain,
|
||||
.text-plain:hover {
|
||||
color: $gl-text-color;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: $gl-text-color-secondary;
|
||||
}
|
||||
|
||||
.text-primary,
|
||||
.text-primary:hover {
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
.text-success,
|
||||
.text-success:hover {
|
||||
color: $brand-success;
|
||||
}
|
||||
|
||||
.text-danger,
|
||||
.text-danger:hover {
|
||||
color: $brand-danger;
|
||||
}
|
||||
|
||||
.text-warning,
|
||||
.text-warning:hover {
|
||||
color: $brand-warning;
|
||||
}
|
||||
|
||||
.text-info,
|
||||
.text-info:hover {
|
||||
color: $brand-info;
|
||||
}
|
||||
|
||||
.underlined-link { text-decoration: underline; }
|
||||
.hint { font-style: italic; color: $hint-color; }
|
||||
.light { color: $common-gray; }
|
||||
|
|
|
|||
|
|
@ -1002,6 +1002,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
|
|||
max-width: 250px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
|
|
|||
|
|
@ -34,8 +34,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
.flash-success {
|
||||
@extend .alert;
|
||||
@extend .alert-success;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.flash-notice,
|
||||
.flash-alert {
|
||||
.flash-alert,
|
||||
.flash-success {
|
||||
border-radius: $border-radius-default;
|
||||
|
||||
.container-fluid,
|
||||
|
|
@ -48,7 +55,8 @@
|
|||
margin-bottom: 0;
|
||||
|
||||
.flash-notice,
|
||||
.flash-alert {
|
||||
.flash-alert,
|
||||
.flash-success {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,35 +1,49 @@
|
|||
.ci-status-icon-success,
|
||||
.ci-status-icon-passed {
|
||||
color: $green-500;
|
||||
svg {
|
||||
fill: $green-500;
|
||||
}
|
||||
}
|
||||
|
||||
.ci-status-icon-failed {
|
||||
color: $gl-danger;
|
||||
svg {
|
||||
fill: $gl-danger;
|
||||
}
|
||||
}
|
||||
|
||||
.ci-status-icon-pending,
|
||||
.ci-status-icon-failed_with_warnings,
|
||||
.ci-status-icon-success_with_warnings {
|
||||
color: $orange-500;
|
||||
svg {
|
||||
fill: $orange-500;
|
||||
}
|
||||
}
|
||||
|
||||
.ci-status-icon-running {
|
||||
color: $blue-400;
|
||||
svg {
|
||||
fill: $blue-400;
|
||||
}
|
||||
}
|
||||
|
||||
.ci-status-icon-canceled,
|
||||
.ci-status-icon-disabled,
|
||||
.ci-status-icon-not-found {
|
||||
color: $gl-text-color;
|
||||
svg {
|
||||
fill: $gl-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.ci-status-icon-created,
|
||||
.ci-status-icon-skipped {
|
||||
color: $gray-darkest;
|
||||
svg {
|
||||
fill: $gray-darkest;
|
||||
}
|
||||
}
|
||||
|
||||
.ci-status-icon-manual {
|
||||
color: $gl-text-color;
|
||||
svg {
|
||||
fill: $gl-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-link {
|
||||
|
|
|
|||
|
|
@ -195,33 +195,6 @@ summary {
|
|||
}
|
||||
}
|
||||
|
||||
// Typography =================================================================
|
||||
|
||||
.text-primary,
|
||||
.text-primary:hover {
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
.text-success,
|
||||
.text-success:hover {
|
||||
color: $brand-success;
|
||||
}
|
||||
|
||||
.text-danger,
|
||||
.text-danger:hover {
|
||||
color: $brand-danger;
|
||||
}
|
||||
|
||||
.text-warning,
|
||||
.text-warning:hover {
|
||||
color: $brand-warning;
|
||||
}
|
||||
|
||||
.text-info,
|
||||
.text-info:hover {
|
||||
color: $brand-info;
|
||||
}
|
||||
|
||||
// Prevent datetimes on tooltips to break into two lines
|
||||
.local-timeago {
|
||||
white-space: nowrap;
|
||||
|
|
|
|||
|
|
@ -70,14 +70,13 @@
|
|||
|
||||
.title {
|
||||
padding: 0;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: $gl-padding;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
margin-left: auto;
|
||||
// Set height to match title height
|
||||
height: 2em;
|
||||
height: $gl-padding * 2;
|
||||
}
|
||||
|
||||
// Border around images in issue and MR descriptions.
|
||||
|
|
|
|||
|
|
@ -203,7 +203,24 @@ ul.related-merge-requests > li {
|
|||
}
|
||||
|
||||
.create-mr-dropdown-wrap {
|
||||
@include new-style-dropdown;
|
||||
.branch-message,
|
||||
.ref-message {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ref::selection {
|
||||
color: $placeholder-text-color;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
.dropdown-menu-toggle {
|
||||
min-width: 285px;
|
||||
}
|
||||
|
||||
.dropdown-select {
|
||||
width: 285px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-group:not(.hide) {
|
||||
display: flex;
|
||||
|
|
@ -214,15 +231,16 @@ ul.related-merge-requests > li {
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
.create-merge-request-dropdown-menu {
|
||||
width: 300px;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
display: none;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
.create-merge-request-dropdown-toggle {
|
||||
.fa-caret-down {
|
||||
pointer-events: none;
|
||||
color: inherit;
|
||||
|
|
@ -230,18 +248,50 @@ ul.related-merge-requests > li {
|
|||
}
|
||||
}
|
||||
|
||||
.droplab-item-ignore {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.create-item {
|
||||
cursor: pointer;
|
||||
margin: 0 1px;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $dropdown-item-hover-bg;
|
||||
color: $gl-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
li.divider {
|
||||
margin: 8px 10px;
|
||||
}
|
||||
|
||||
li:not(.divider) {
|
||||
padding: 8px 9px;
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
&.droplab-item-selected {
|
||||
.icon-container {
|
||||
i {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.droplab-item-ignore {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
float: left;
|
||||
padding-left: 6px;
|
||||
|
||||
i {
|
||||
visibility: hidden;
|
||||
|
|
@ -249,13 +299,12 @@ ul.related-merge-requests > li {
|
|||
}
|
||||
|
||||
.description {
|
||||
padding-left: 30px;
|
||||
font-size: 13px;
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
font-weight: $gl-font-weight-bold;
|
||||
}
|
||||
input,
|
||||
span {
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -543,10 +543,7 @@ ul.notes {
|
|||
}
|
||||
|
||||
svg {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
top: 0;
|
||||
vertical-align: text-top;
|
||||
@include btn-svg;
|
||||
}
|
||||
|
||||
.award-control-icon-positive,
|
||||
|
|
@ -780,12 +777,6 @@ ul.notes {
|
|||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: currentColor;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
margin: 0;
|
||||
height: auto;
|
||||
|
|
|
|||
|
|
@ -291,14 +291,7 @@
|
|||
}
|
||||
|
||||
svg {
|
||||
|
||||
path {
|
||||
fill: $layout-link-gray;
|
||||
}
|
||||
|
||||
use {
|
||||
stroke: $layout-link-gray;
|
||||
}
|
||||
fill: $layout-link-gray;
|
||||
}
|
||||
|
||||
.fa-caret-down {
|
||||
|
|
@ -402,6 +395,18 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.clone-dropdown-btn {
|
||||
background-color: $white-light;
|
||||
}
|
||||
|
||||
.clone-options-dropdown {
|
||||
min-width: 240px;
|
||||
|
||||
.dropdown-menu-inner-content {
|
||||
min-width: 320px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.project-repo-buttons {
|
||||
|
|
@ -886,10 +891,6 @@ pre.light-well {
|
|||
font-size: $gl-font-size;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $gl-text-color;
|
||||
}
|
||||
|
||||
.avatar-container,
|
||||
.controls {
|
||||
flex: 0 0 auto;
|
||||
|
|
|
|||
|
|
@ -35,275 +35,247 @@
|
|||
}
|
||||
}
|
||||
|
||||
.repository-view {
|
||||
border: 1px solid $border-color;
|
||||
border-radius: $border-radius-default;
|
||||
color: $almost-black;
|
||||
.multi-file {
|
||||
display: flex;
|
||||
height: calc(100vh - 145px);
|
||||
border-top: 1px solid $white-dark;
|
||||
border-bottom: 1px solid $white-dark;
|
||||
|
||||
.code.white pre .hll {
|
||||
background-color: $well-light-border !important;
|
||||
}
|
||||
|
||||
.tree-content-holder {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.tree-content-holder-mini {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.panel-right {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
width: 80%;
|
||||
height: 100%;
|
||||
|
||||
.monaco-editor.vs {
|
||||
.current-line {
|
||||
border: 0;
|
||||
background: $well-light-border;
|
||||
}
|
||||
|
||||
.line-numbers {
|
||||
cursor: pointer;
|
||||
min-width: initial;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.blob-no-preview {
|
||||
.vertical-center {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&.blob-editor-container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.blob-viewer-container {
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
> div,
|
||||
.file-content:not(.wiki) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
> div,
|
||||
.file-content,
|
||||
.blob-viewer,
|
||||
.line-number,
|
||||
.blob-content,
|
||||
.code {
|
||||
min-height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.line-numbers {
|
||||
min-width: 44px;
|
||||
}
|
||||
|
||||
.blob-content {
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
#tabs {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding-left: 0;
|
||||
margin-bottom: 0;
|
||||
white-space: nowrap;
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
background: $gray-normal;
|
||||
padding: #{$gl-padding / 2} $gl-padding;
|
||||
border-right: 1px solid $white-dark;
|
||||
border-bottom: 1px solid $white-dark;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
background: $white-light;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
@include str-truncated(100px);
|
||||
color: $gl-text-color;
|
||||
vertical-align: middle;
|
||||
text-decoration: none;
|
||||
margin-right: 12px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: 0;
|
||||
font-size: $gl-font-size;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.close-icon:hover {
|
||||
color: $hint-color;
|
||||
}
|
||||
|
||||
.close-icon,
|
||||
.unsaved-icon {
|
||||
color: $gray-darkest;
|
||||
}
|
||||
|
||||
.unsaved-icon {
|
||||
color: $brand-success;
|
||||
}
|
||||
|
||||
&.tabs-divider {
|
||||
width: 100%;
|
||||
background-color: $white-light;
|
||||
border-right: 0;
|
||||
border-top-right-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.repo-file-buttons {
|
||||
background-color: $white-light;
|
||||
padding: 5px 10px;
|
||||
border-top: 1px solid $white-normal;
|
||||
}
|
||||
|
||||
#binary-viewer {
|
||||
height: 80vh;
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
|
||||
.blob-viewer {
|
||||
padding-top: 20px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.binary-unknown {
|
||||
text-align: center;
|
||||
padding-top: 100px;
|
||||
background: $gray-light;
|
||||
height: 100%;
|
||||
font-size: 17px;
|
||||
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#commit-area {
|
||||
background: $gray-light;
|
||||
padding: 20px;
|
||||
|
||||
.help-block {
|
||||
padding-top: 7px;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#view-toggler {
|
||||
height: 41px;
|
||||
position: relative;
|
||||
display: block;
|
||||
border-bottom: 1px solid $white-normal;
|
||||
background: $white-light;
|
||||
margin-top: -5px;
|
||||
}
|
||||
|
||||
#binary-viewer {
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
|
||||
&.sidebar-mini {
|
||||
width: 20%;
|
||||
border-right: 1px solid $white-normal;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
tr {
|
||||
.repo-file-options {
|
||||
padding: 2px 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.file {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a {
|
||||
@include str-truncated(250px);
|
||||
color: $almost-black;
|
||||
&.is-collapsed {
|
||||
.ide-file-list {
|
||||
max-width: 250px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.render-error {
|
||||
min-height: calc(100vh - 62px);
|
||||
.ide-file-list {
|
||||
flex: 1;
|
||||
overflow: scroll;
|
||||
|
||||
p {
|
||||
width: 100%;
|
||||
.file {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $gl-text-color;
|
||||
}
|
||||
|
||||
th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.multi-file-table-col-name {
|
||||
.multi-file-table-name,
|
||||
.multi-file-table-col-commit-message {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 0;
|
||||
}
|
||||
|
||||
.multi-file-table-name {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.multi-file-table-col-commit-message {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.multi-file-edit-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
border-left: 1px solid $white-dark;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.multi-file-tabs {
|
||||
display: flex;
|
||||
overflow: scroll;
|
||||
background-color: $white-normal;
|
||||
box-shadow: inset 0 -1px $white-dark;
|
||||
|
||||
> li {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.multi-file-tab {
|
||||
@include str-truncated(150px);
|
||||
padding: ($gl-padding / 2) ($gl-padding + 12) ($gl-padding / 2) $gl-padding;
|
||||
background-color: $gray-normal;
|
||||
border-right: 1px solid $white-dark;
|
||||
border-bottom: 1px solid $white-dark;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
background-color: $white-light;
|
||||
border-bottom-color: $white-light;
|
||||
}
|
||||
}
|
||||
|
||||
.multi-file-tab-close {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: 0;
|
||||
font-size: $gl-font-size;
|
||||
color: $gray-darkest;
|
||||
transform: translateY(-50%);
|
||||
|
||||
&:not(.modified):hover,
|
||||
&:not(.modified):focus {
|
||||
color: $hint-color;
|
||||
}
|
||||
|
||||
&.modified {
|
||||
color: $indigo-700;
|
||||
}
|
||||
}
|
||||
|
||||
.multi-file-edit-pane-content {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.multi-file-editor-btn-group {
|
||||
padding: $grid-size;
|
||||
border-top: 1px solid $white-dark;
|
||||
}
|
||||
|
||||
// Not great, but this is to deal with our current output
|
||||
.multi-file-preview-holder {
|
||||
height: 100%;
|
||||
overflow: scroll;
|
||||
|
||||
.blob-viewer {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.file-content.code {
|
||||
display: flex;
|
||||
|
||||
i {
|
||||
margin-left: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
.line-numbers {
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.file-content,
|
||||
.line-numbers,
|
||||
.blob-content,
|
||||
.code {
|
||||
min-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.multi-file-commit-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 290px;
|
||||
padding: $gl-padding;
|
||||
background-color: $gray-light;
|
||||
border-left: 1px solid $white-dark;
|
||||
|
||||
&.is-collapsed {
|
||||
width: 60px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.multi-file-commit-panel-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.multi-file-commit-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 0 12px;
|
||||
margin-bottom: 12px;
|
||||
border-bottom: 1px solid $white-dark;
|
||||
|
||||
&.is-collapsed {
|
||||
border-bottom: 1px solid $white-dark;
|
||||
|
||||
svg {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.multi-file-commit-panel-collapse-btn {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-left: auto;
|
||||
font-size: 20px;
|
||||
|
||||
&.is-collapsed {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.multi-file-commit-list {
|
||||
flex: 1;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.multi-file-commit-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.multi-file-addition {
|
||||
fill: $green-500;
|
||||
}
|
||||
|
||||
.multi-file-modified {
|
||||
fill: $orange-500;
|
||||
}
|
||||
|
||||
.multi-file-commit-list-collapsed {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> svg {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.multi-file-commit-list-path {
|
||||
@include str-truncated(100%);
|
||||
}
|
||||
|
||||
.multi-file-commit-form {
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid $white-dark;
|
||||
}
|
||||
|
||||
.multi-file-commit-fieldset {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 12px;
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.multi-file-commit-message.form-control {
|
||||
height: 80px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.dirty-diff {
|
||||
// !important need to override monaco inline style
|
||||
width: 4px !important;
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ module IssuableActions
|
|||
end
|
||||
|
||||
def destroy
|
||||
Issuable::DestroyService.new(project, current_user).execute(issuable)
|
||||
Issuable::DestroyService.new(issuable.project, current_user).execute(issuable)
|
||||
TodoService.new.destroy_issuable(issuable, current_user)
|
||||
|
||||
name = issuable.human_class_name
|
||||
|
|
|
|||
|
|
@ -158,7 +158,8 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def create_merge_request
|
||||
result = ::MergeRequests::CreateFromIssueService.new(project, current_user, issue_iid: issue.iid).execute
|
||||
create_params = params.slice(:branch_name, :ref).merge(issue_iid: issue.iid)
|
||||
result = ::MergeRequests::CreateFromIssueService.new(project, current_user, create_params).execute
|
||||
|
||||
if result[:status] == :success
|
||||
render json: MergeRequestCreateSerializer.new.represent(result[:merge_request])
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
|
|||
|
||||
if params[:ref].present?
|
||||
@ref = params[:ref]
|
||||
@commit = @repository.commit("refs/heads/#{@ref}")
|
||||
@commit = @repository.commit(Gitlab::Git::BRANCH_REF_PREFIX + @ref)
|
||||
end
|
||||
|
||||
render layout: false
|
||||
|
|
@ -76,7 +76,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
|
|||
|
||||
if params[:ref].present?
|
||||
@ref = params[:ref]
|
||||
@commit = @target_project.commit("refs/heads/#{@ref}")
|
||||
@commit = @target_project.commit(Gitlab::Git::BRANCH_REF_PREFIX + @ref)
|
||||
end
|
||||
|
||||
render layout: false
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
|
|||
@merge_request.merge_request_diff
|
||||
end
|
||||
|
||||
@merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff.order_id_desc
|
||||
@merge_request_diffs = @merge_request.merge_request_diffs.viewable.order_id_desc
|
||||
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
|
||||
|
||||
if params[:start_sha].present?
|
||||
|
|
|
|||
|
|
@ -6,11 +6,19 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def update
|
||||
if @project.update(update_params)
|
||||
flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
|
||||
redirect_to project_settings_ci_cd_path(@project)
|
||||
else
|
||||
render 'show'
|
||||
Projects::UpdateService.new(project, current_user, update_params).tap do |service|
|
||||
if service.execute
|
||||
flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
|
||||
|
||||
if service.run_auto_devops_pipeline?
|
||||
CreatePipelineWorker.perform_async(project.id, current_user.id, project.default_branch, :web, ignore_skip_ci: true, save_on_errors: false)
|
||||
flash[:success] = "A new Auto DevOps pipeline has been created, go to <a href=\"#{project_pipelines_path(@project)}\">Pipelines page</a> for details".html_safe
|
||||
end
|
||||
|
||||
redirect_to project_settings_ci_cd_path(@project)
|
||||
else
|
||||
render 'show'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -21,6 +29,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
|
|||
:runners_token, :builds_enabled, :build_allow_git_fetch,
|
||||
:build_timeout_in_minutes, :build_coverage_regex, :public_builds,
|
||||
:auto_cancel_pending_pipelines, :ci_config_path,
|
||||
:run_auto_devops_pipeline_implicit, :run_auto_devops_pipeline_explicit,
|
||||
auto_devops_attributes: [:id, :domain, :enabled]
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -104,8 +104,7 @@ class NotesFinder
|
|||
query = @params[:search]
|
||||
return notes unless query
|
||||
|
||||
pattern = "%#{query}%"
|
||||
notes.where(Note.arel_table[:note].matches(pattern))
|
||||
notes.search(query)
|
||||
end
|
||||
|
||||
# Notes changed since last fetch
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
class RunnerJobsFinder
|
||||
attr_reader :runner, :params
|
||||
|
||||
def initialize(runner, params = {})
|
||||
@runner = runner
|
||||
@params = params
|
||||
end
|
||||
|
||||
def execute
|
||||
items = @runner.builds
|
||||
items = by_status(items)
|
||||
items
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def by_status(items)
|
||||
return items unless HasStatus::AVAILABLE_STATUSES.include?(params[:status])
|
||||
|
||||
items.where(status: params[:status])
|
||||
end
|
||||
end
|
||||
|
|
@ -30,9 +30,9 @@ module ApplicationSettingsHelper
|
|||
def enabled_project_button(project, protocol)
|
||||
case protocol
|
||||
when 'ssh'
|
||||
ssh_clone_button(project, 'bottom', append_link: false)
|
||||
ssh_clone_button(project, append_link: false)
|
||||
else
|
||||
http_clone_button(project, 'bottom', append_link: false)
|
||||
http_clone_button(project, append_link: false)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -177,6 +177,9 @@ module ApplicationSettingsHelper
|
|||
:ed25519_key_restriction,
|
||||
:email_author_in_body,
|
||||
:enabled_git_access_protocol,
|
||||
:gitaly_timeout_default,
|
||||
:gitaly_timeout_medium,
|
||||
:gitaly_timeout_fast,
|
||||
:gravatar_enabled,
|
||||
:hashed_storage_enabled,
|
||||
:help_page_hide_commercial_content,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,22 @@ module AutoDevopsHelper
|
|||
!project.ci_service
|
||||
end
|
||||
|
||||
def show_run_auto_devops_pipeline_checkbox_for_instance_setting?(project)
|
||||
return false if project.repository.gitlab_ci_yml
|
||||
|
||||
if project&.auto_devops&.enabled.present?
|
||||
!project.auto_devops.enabled && current_application_settings.auto_devops_enabled?
|
||||
else
|
||||
current_application_settings.auto_devops_enabled?
|
||||
end
|
||||
end
|
||||
|
||||
def show_run_auto_devops_pipeline_checkbox_for_explicit_setting?(project)
|
||||
return false if project.repository.gitlab_ci_yml
|
||||
|
||||
!project.auto_devops_enabled?
|
||||
end
|
||||
|
||||
def auto_devops_warning_message(project)
|
||||
missing_domain = !project.auto_devops&.has_domain?
|
||||
missing_service = !project.kubernetes_service&.active?
|
||||
|
|
|
|||
|
|
@ -56,42 +56,36 @@ module ButtonHelper
|
|||
end
|
||||
end
|
||||
|
||||
def http_clone_button(project, placement = 'right', append_link: true)
|
||||
klass = 'http-selector'
|
||||
klass << ' has-tooltip' if current_user.try(:require_extra_setup_for_git_auth?)
|
||||
|
||||
def http_clone_button(project, append_link: true)
|
||||
protocol = gitlab_config.protocol.upcase
|
||||
dropdown_description = http_dropdown_description(protocol)
|
||||
append_url = project.http_url_to_repo if append_link
|
||||
|
||||
tooltip_title =
|
||||
if current_user.try(:require_password_creation_for_git?)
|
||||
_("Set a password on your account to pull or push via %{protocol}.") % { protocol: protocol }
|
||||
else
|
||||
_("Create a personal access token on your account to pull or push via %{protocol}.") % { protocol: protocol }
|
||||
end
|
||||
|
||||
content_tag (append_link ? :a : :span), protocol,
|
||||
class: klass,
|
||||
href: (project.http_url_to_repo if append_link),
|
||||
data: {
|
||||
html: true,
|
||||
placement: placement,
|
||||
container: 'body',
|
||||
title: tooltip_title
|
||||
}
|
||||
dropdown_item_with_description(protocol, dropdown_description, href: append_url)
|
||||
end
|
||||
|
||||
def ssh_clone_button(project, placement = 'right', append_link: true)
|
||||
klass = 'ssh-selector'
|
||||
klass << ' has-tooltip' if current_user.try(:require_ssh_key?)
|
||||
def http_dropdown_description(protocol)
|
||||
if current_user.try(:require_password_creation_for_git?)
|
||||
_("Set a password on your account to pull or push via %{protocol}.") % { protocol: protocol }
|
||||
else
|
||||
_("Create a personal access token on your account to pull or push via %{protocol}.") % { protocol: protocol }
|
||||
end
|
||||
end
|
||||
|
||||
content_tag (append_link ? :a : :span), 'SSH',
|
||||
class: klass,
|
||||
href: (project.ssh_url_to_repo if append_link),
|
||||
data: {
|
||||
html: true,
|
||||
placement: placement,
|
||||
container: 'body',
|
||||
title: _('Add an SSH key to your profile to pull or push via SSH.')
|
||||
}
|
||||
def ssh_clone_button(project, append_link: true)
|
||||
dropdown_description = _("You won't be able to pull or push project code via SSH until you add an SSH key to your profile") if current_user.try(:require_ssh_key?)
|
||||
append_url = project.ssh_url_to_repo if append_link
|
||||
|
||||
dropdown_item_with_description('SSH', dropdown_description, href: append_url)
|
||||
end
|
||||
|
||||
def dropdown_item_with_description(title, description, href: nil)
|
||||
button_content = content_tag(:strong, title, class: 'dropdown-menu-inner-title')
|
||||
button_content << content_tag(:span, description, class: 'dropdown-menu-inner-content') if description
|
||||
|
||||
content_tag (href ? :a : :span),
|
||||
button_content,
|
||||
class: "#{title.downcase}-selector",
|
||||
href: (href if href)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -212,6 +212,7 @@ module IssuablesHelper
|
|||
def issuable_initial_data(issuable)
|
||||
data = {
|
||||
endpoint: issuable_path(issuable),
|
||||
updateEndpoint: "#{issuable_path(issuable)}.json",
|
||||
canUpdate: can?(current_user, :"update_#{issuable.to_ability_name}", issuable),
|
||||
canDestroy: can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable),
|
||||
issuableRef: issuable.to_reference,
|
||||
|
|
|
|||
|
|
@ -172,6 +172,27 @@ class ApplicationSetting < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
validates :gitaly_timeout_default,
|
||||
presence: true,
|
||||
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
||||
|
||||
validates :gitaly_timeout_medium,
|
||||
presence: true,
|
||||
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
||||
validates :gitaly_timeout_medium,
|
||||
numericality: { less_than_or_equal_to: :gitaly_timeout_default },
|
||||
if: :gitaly_timeout_default
|
||||
validates :gitaly_timeout_medium,
|
||||
numericality: { greater_than_or_equal_to: :gitaly_timeout_fast },
|
||||
if: :gitaly_timeout_fast
|
||||
|
||||
validates :gitaly_timeout_fast,
|
||||
presence: true,
|
||||
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
||||
validates :gitaly_timeout_fast,
|
||||
numericality: { less_than_or_equal_to: :gitaly_timeout_default },
|
||||
if: :gitaly_timeout_default
|
||||
|
||||
SUPPORTED_KEY_TYPES.each do |type|
|
||||
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
|
||||
end
|
||||
|
|
@ -308,7 +329,10 @@ class ApplicationSetting < ActiveRecord::Base
|
|||
two_factor_grace_period: 48,
|
||||
user_default_external: false,
|
||||
polling_interval_multiplier: 1,
|
||||
usage_ping_enabled: Settings.gitlab['usage_ping_enabled']
|
||||
usage_ping_enabled: Settings.gitlab['usage_ping_enabled'],
|
||||
gitaly_timeout_fast: 10,
|
||||
gitaly_timeout_medium: 30,
|
||||
gitaly_timeout_default: 55
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
module Ci
|
||||
class Runner < ActiveRecord::Base
|
||||
extend Gitlab::Ci::Model
|
||||
include Gitlab::SQL::Pattern
|
||||
|
||||
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
|
||||
ONLINE_CONTACT_TIMEOUT = 1.hour
|
||||
|
|
@ -59,10 +60,7 @@ module Ci
|
|||
#
|
||||
# Returns an ActiveRecord::Relation.
|
||||
def self.search(query)
|
||||
t = arel_table
|
||||
pattern = "%#{query}%"
|
||||
|
||||
where(t[:token].matches(pattern).or(t[:description].matches(pattern)))
|
||||
fuzzy_search(query, [:token, :description])
|
||||
end
|
||||
|
||||
def self.contact_time_deadline
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ module HasVariable
|
|||
key: Gitlab::Application.secrets.db_key_base,
|
||||
algorithm: 'aes-256-cbc'
|
||||
|
||||
def key=(new_key)
|
||||
super(new_key.to_s.strip)
|
||||
end
|
||||
|
||||
def to_runner_variable
|
||||
{ key: key, value: value, public: false }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -122,9 +122,7 @@ module Issuable
|
|||
#
|
||||
# Returns an ActiveRecord::Relation.
|
||||
def search(query)
|
||||
title = to_fuzzy_arel(:title, query)
|
||||
|
||||
where(title)
|
||||
fuzzy_search(query, [:title])
|
||||
end
|
||||
|
||||
# Searches for records with a matching title or description.
|
||||
|
|
@ -135,10 +133,7 @@ module Issuable
|
|||
#
|
||||
# Returns an ActiveRecord::Relation.
|
||||
def full_search(query)
|
||||
title = to_fuzzy_arel(:title, query)
|
||||
description = to_fuzzy_arel(:description, query)
|
||||
|
||||
where(title&.or(description))
|
||||
fuzzy_search(query, [:title, :description])
|
||||
end
|
||||
|
||||
def sort(method, excluded_labels: [])
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
class Email < ActiveRecord::Base
|
||||
include Sortable
|
||||
include Gitlab::SQL::Pattern
|
||||
|
||||
belongs_to :user
|
||||
|
||||
|
|
|
|||
|
|
@ -50,20 +50,6 @@ class Group < Namespace
|
|||
Gitlab::Database.postgresql?
|
||||
end
|
||||
|
||||
# Searches for groups matching the given query.
|
||||
#
|
||||
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
|
||||
#
|
||||
# query - The search query as a String
|
||||
#
|
||||
# Returns an ActiveRecord::Relation.
|
||||
def search(query)
|
||||
table = Namespace.arel_table
|
||||
pattern = "%#{query}%"
|
||||
|
||||
where(table[:name].matches(pattern).or(table[:path].matches(pattern)))
|
||||
end
|
||||
|
||||
def sort(method)
|
||||
if method == 'storage_size_desc'
|
||||
# storage_size is a virtual column so we need to
|
||||
|
|
|
|||
|
|
@ -283,9 +283,9 @@ class MergeRequest < ActiveRecord::Base
|
|||
if persisted?
|
||||
merge_request_diff.commit_shas
|
||||
elsif compare_commits
|
||||
compare_commits.reverse.map(&:sha)
|
||||
compare_commits.to_a.reverse.map(&:sha)
|
||||
else
|
||||
[]
|
||||
Array(diff_head_sha)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -364,16 +364,28 @@ class MergeRequest < ActiveRecord::Base
|
|||
# We use these attributes to force these to the intended values.
|
||||
attr_writer :target_branch_sha, :source_branch_sha
|
||||
|
||||
def source_branch_ref
|
||||
return @source_branch_sha if @source_branch_sha
|
||||
return unless source_branch
|
||||
|
||||
Gitlab::Git::BRANCH_REF_PREFIX + source_branch
|
||||
end
|
||||
|
||||
def target_branch_ref
|
||||
return @target_branch_sha if @target_branch_sha
|
||||
return unless target_branch
|
||||
|
||||
Gitlab::Git::BRANCH_REF_PREFIX + target_branch
|
||||
end
|
||||
|
||||
def source_branch_head
|
||||
return unless source_project
|
||||
|
||||
source_branch_ref = @source_branch_sha || source_branch
|
||||
source_project.repository.commit(source_branch_ref) if source_branch_ref
|
||||
end
|
||||
|
||||
def target_branch_head
|
||||
target_branch_ref = @target_branch_sha || target_branch
|
||||
target_project.repository.commit(target_branch_ref) if target_branch_ref
|
||||
target_project.repository.commit(target_branch_ref)
|
||||
end
|
||||
|
||||
def branch_merge_base_commit
|
||||
|
|
@ -482,7 +494,7 @@ class MergeRequest < ActiveRecord::Base
|
|||
|
||||
def merge_request_diff_for(diff_refs_or_sha)
|
||||
@merge_request_diffs_by_diff_refs_or_sha ||= Hash.new do |h, diff_refs_or_sha|
|
||||
diffs = merge_request_diffs.viewable.select_without_diff
|
||||
diffs = merge_request_diffs.viewable
|
||||
h[diff_refs_or_sha] =
|
||||
if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs)
|
||||
diffs.find_by_diff_refs(diff_refs_or_sha)
|
||||
|
|
@ -887,7 +899,8 @@ class MergeRequest < ActiveRecord::Base
|
|||
def compute_diverged_commits_count
|
||||
return 0 unless source_branch_sha && target_branch_sha
|
||||
|
||||
Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_branch_sha, target_branch_sha).size
|
||||
target_project.repository
|
||||
.count_commits_between(source_branch_sha, target_branch_sha)
|
||||
end
|
||||
private :compute_diverged_commits_count
|
||||
|
||||
|
|
@ -906,28 +919,18 @@ class MergeRequest < ActiveRecord::Base
|
|||
# Note that this could also return SHA from now dangling commits
|
||||
#
|
||||
def all_commit_shas
|
||||
if persisted?
|
||||
# MySQL doesn't support LIMIT in a subquery.
|
||||
diffs_relation =
|
||||
if Gitlab::Database.postgresql?
|
||||
merge_request_diffs.order(id: :desc).limit(100)
|
||||
else
|
||||
merge_request_diffs
|
||||
end
|
||||
return commit_shas unless persisted?
|
||||
|
||||
column_shas = MergeRequestDiffCommit
|
||||
.where(merge_request_diff: diffs_relation)
|
||||
.limit(10_000)
|
||||
.pluck('sha')
|
||||
diffs_relation = merge_request_diffs
|
||||
|
||||
serialised_shas = merge_request_diffs.where.not(st_commits: nil).flat_map(&:commit_shas)
|
||||
# MySQL doesn't support LIMIT in a subquery.
|
||||
diffs_relation = diffs_relation.recent if Gitlab::Database.postgresql?
|
||||
|
||||
(column_shas + serialised_shas).uniq
|
||||
elsif compare_commits
|
||||
compare_commits.to_a.reverse.map(&:id)
|
||||
else
|
||||
[diff_head_sha]
|
||||
end
|
||||
MergeRequestDiffCommit
|
||||
.where(merge_request_diff: diffs_relation)
|
||||
.limit(10_000)
|
||||
.pluck('sha')
|
||||
.uniq
|
||||
end
|
||||
|
||||
def merge_commit
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
class MergeRequestDiff < ActiveRecord::Base
|
||||
include Sortable
|
||||
include Importable
|
||||
include Gitlab::EncodingHelper
|
||||
include ManualInverseAssociation
|
||||
include IgnorableColumn
|
||||
|
||||
# Prevent store of diff if commits amount more then 500
|
||||
# Don't display more than 100 commits at once
|
||||
COMMITS_SAFE_SIZE = 100
|
||||
|
||||
# Valid types of serialized diffs allowed by Gitlab::Git::Diff
|
||||
VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta].freeze
|
||||
ignore_column :st_commits,
|
||||
:st_diffs
|
||||
|
||||
belongs_to :merge_request
|
||||
manual_inverse_association :merge_request, :merge_request_diff
|
||||
|
|
@ -16,9 +16,6 @@ class MergeRequestDiff < ActiveRecord::Base
|
|||
has_many :merge_request_diff_files, -> { order(:merge_request_diff_id, :relative_order) }
|
||||
has_many :merge_request_diff_commits, -> { order(:merge_request_diff_id, :relative_order) }
|
||||
|
||||
serialize :st_commits # rubocop:disable Cop/ActiveRecordSerialize
|
||||
serialize :st_diffs # rubocop:disable Cop/ActiveRecordSerialize
|
||||
|
||||
state_machine :state, initial: :empty do
|
||||
state :collected
|
||||
state :overflow
|
||||
|
|
@ -32,6 +29,8 @@ class MergeRequestDiff < ActiveRecord::Base
|
|||
|
||||
scope :viewable, -> { without_state(:empty) }
|
||||
|
||||
scope :recent, -> { order(id: :desc).limit(100) }
|
||||
|
||||
# All diff information is collected from repository after object is created.
|
||||
# It allows you to override variables like head_commit_sha before getting diff.
|
||||
after_create :save_git_content, unless: :importing?
|
||||
|
|
@ -40,14 +39,6 @@ class MergeRequestDiff < ActiveRecord::Base
|
|||
find_by(start_commit_sha: diff_refs.start_sha, head_commit_sha: diff_refs.head_sha, base_commit_sha: diff_refs.base_sha)
|
||||
end
|
||||
|
||||
def self.select_without_diff
|
||||
select(column_names - ['st_diffs'])
|
||||
end
|
||||
|
||||
def st_commits
|
||||
super || []
|
||||
end
|
||||
|
||||
# Collect information about commits and diff from repository
|
||||
# and save it to the database as serialized data
|
||||
def save_git_content
|
||||
|
|
@ -129,11 +120,7 @@ class MergeRequestDiff < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def commit_shas
|
||||
if st_commits.present?
|
||||
st_commits.map { |commit| commit[:id] }
|
||||
else
|
||||
merge_request_diff_commits.map(&:sha)
|
||||
end
|
||||
merge_request_diff_commits.map(&:sha)
|
||||
end
|
||||
|
||||
def diff_refs=(new_diff_refs)
|
||||
|
|
@ -208,34 +195,11 @@ class MergeRequestDiff < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def commits_count
|
||||
if st_commits.present?
|
||||
st_commits.size
|
||||
else
|
||||
merge_request_diff_commits.size
|
||||
end
|
||||
end
|
||||
|
||||
def utf8_st_diffs
|
||||
return [] if st_diffs.blank?
|
||||
|
||||
st_diffs.map do |diff|
|
||||
diff.each do |k, v|
|
||||
diff[k] = encode_utf8(v) if v.respond_to?(:encoding)
|
||||
end
|
||||
end
|
||||
merge_request_diff_commits.size
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Old GitLab implementations may have generated diffs as ["--broken-diff"].
|
||||
# Avoid an error 500 by ignoring bad elements. See:
|
||||
# https://gitlab.com/gitlab-org/gitlab-ce/issues/20776
|
||||
def valid_raw_diff?(raw)
|
||||
return false unless raw.respond_to?(:each)
|
||||
|
||||
raw.any? { |element| VALID_CLASSES.include?(element.class) }
|
||||
end
|
||||
|
||||
def create_merge_request_diff_files(diffs)
|
||||
rows = diffs.map.with_index do |diff, index|
|
||||
diff_hash = diff.to_hash.merge(
|
||||
|
|
@ -259,9 +223,7 @@ class MergeRequestDiff < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def load_diffs(options)
|
||||
return Gitlab::Git::DiffCollection.new([]) unless diffs_from_database
|
||||
|
||||
raw = diffs_from_database
|
||||
raw = merge_request_diff_files.map(&:to_hash)
|
||||
|
||||
if paths = options[:paths]
|
||||
raw = raw.select do |diff|
|
||||
|
|
@ -272,22 +234,8 @@ class MergeRequestDiff < ActiveRecord::Base
|
|||
Gitlab::Git::DiffCollection.new(raw, options)
|
||||
end
|
||||
|
||||
def diffs_from_database
|
||||
return @diffs_from_database if defined?(@diffs_from_database)
|
||||
|
||||
@diffs_from_database =
|
||||
if st_diffs.present?
|
||||
if valid_raw_diff?(st_diffs)
|
||||
st_diffs
|
||||
end
|
||||
elsif merge_request_diff_files.present?
|
||||
merge_request_diff_files.map(&:to_hash)
|
||||
end
|
||||
end
|
||||
|
||||
def load_commits
|
||||
commits = st_commits.presence || merge_request_diff_commits
|
||||
commits = commits.map { |commit| Commit.from_hash(commit.to_hash, project) }
|
||||
commits = merge_request_diff_commits.map { |commit| Commit.from_hash(commit.to_hash, project) }
|
||||
|
||||
CommitCollection
|
||||
.new(merge_request.source_project, commits, merge_request.source_branch)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ class Milestone < ActiveRecord::Base
|
|||
include Referable
|
||||
include StripAttribute
|
||||
include Milestoneish
|
||||
include Gitlab::SQL::Pattern
|
||||
|
||||
cache_markdown_field :title, pipeline: :single_line
|
||||
cache_markdown_field :description
|
||||
|
|
@ -73,10 +74,7 @@ class Milestone < ActiveRecord::Base
|
|||
#
|
||||
# Returns an ActiveRecord::Relation.
|
||||
def search(query)
|
||||
t = arel_table
|
||||
pattern = "%#{query}%"
|
||||
|
||||
where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
|
||||
fuzzy_search(query, [:title, :description])
|
||||
end
|
||||
|
||||
def filter_by_state(milestones, state)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ class Namespace < ActiveRecord::Base
|
|||
include Routable
|
||||
include AfterCommitQueue
|
||||
include Storage::LegacyNamespace
|
||||
include Gitlab::SQL::Pattern
|
||||
|
||||
# Prevent users from creating unreasonably deep level of nesting.
|
||||
# The number 20 was taken based on maximum nesting level of
|
||||
|
|
@ -86,10 +87,7 @@ class Namespace < ActiveRecord::Base
|
|||
#
|
||||
# Returns an ActiveRecord::Relation
|
||||
def search(query)
|
||||
t = arel_table
|
||||
pattern = "%#{query}%"
|
||||
|
||||
where(t[:name].matches(pattern).or(t[:path].matches(pattern)))
|
||||
fuzzy_search(query, [:name, :path])
|
||||
end
|
||||
|
||||
def clean_path(path)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class Note < ActiveRecord::Base
|
|||
include ResolvableNote
|
||||
include IgnorableColumn
|
||||
include Editable
|
||||
include Gitlab::SQL::Pattern
|
||||
|
||||
module SpecialRole
|
||||
FIRST_TIME_CONTRIBUTOR = :first_time_contributor
|
||||
|
|
@ -167,6 +168,10 @@ class Note < ActiveRecord::Base
|
|||
def has_special_role?(role, note)
|
||||
note.special_role == role
|
||||
end
|
||||
|
||||
def search(query)
|
||||
fuzzy_search(query, [:note])
|
||||
end
|
||||
end
|
||||
|
||||
def cross_reference?
|
||||
|
|
|
|||
|
|
@ -273,8 +273,9 @@ class Project < ActiveRecord::Base
|
|||
scope :pending_delete, -> { where(pending_delete: true) }
|
||||
scope :without_deleted, -> { where(pending_delete: false) }
|
||||
|
||||
scope :with_hashed_storage, -> { where('storage_version >= 1') }
|
||||
scope :with_legacy_storage, -> { where(storage_version: [nil, 0]) }
|
||||
scope :with_storage_feature, ->(feature) { where('storage_version >= :version', version: HASHED_STORAGE_FEATURES[feature]) }
|
||||
scope :without_storage_feature, ->(feature) { where('storage_version < :version OR storage_version IS NULL', version: HASHED_STORAGE_FEATURES[feature]) }
|
||||
scope :with_unmigrated_storage, -> { where('storage_version < :version OR storage_version IS NULL', version: LATEST_STORAGE_VERSION) }
|
||||
|
||||
scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
|
||||
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
|
||||
|
|
@ -425,17 +426,11 @@ class Project < ActiveRecord::Base
|
|||
#
|
||||
# query - The search query as a String.
|
||||
def search(query)
|
||||
pattern = to_pattern(query)
|
||||
|
||||
where(
|
||||
arel_table[:path].matches(pattern)
|
||||
.or(arel_table[:name].matches(pattern))
|
||||
.or(arel_table[:description].matches(pattern))
|
||||
)
|
||||
fuzzy_search(query, [:path, :name, :description])
|
||||
end
|
||||
|
||||
def search_by_title(query)
|
||||
non_archived.where(arel_table[:name].matches(to_pattern(query)))
|
||||
non_archived.fuzzy_search(query, [:name])
|
||||
end
|
||||
|
||||
def visibility_levels
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ class ProtectedBranch < ActiveRecord::Base
|
|||
def self.protected?(project, ref_name)
|
||||
return true if project.empty_repo? && default_branch_protected?
|
||||
|
||||
self.matching(ref_name, protected_refs: project.protected_branches).present?
|
||||
refs = project.protected_branches.select(:name)
|
||||
|
||||
self.matching(ref_name, protected_refs: refs).present?
|
||||
end
|
||||
|
||||
def self.default_branch_protected?
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ class ProtectedTag < ActiveRecord::Base
|
|||
protected_ref_access_levels :create
|
||||
|
||||
def self.protected?(project, ref_name)
|
||||
self.matching(ref_name, protected_refs: project.protected_tags).present?
|
||||
refs = project.protected_tags.select(:name)
|
||||
|
||||
self.matching(ref_name, protected_refs: refs).present?
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ class Snippet < ActiveRecord::Base
|
|||
include Mentionable
|
||||
include Spammable
|
||||
include Editable
|
||||
include Gitlab::SQL::Pattern
|
||||
|
||||
extend Gitlab::CurrentSettings
|
||||
|
||||
|
|
@ -135,10 +136,7 @@ class Snippet < ActiveRecord::Base
|
|||
#
|
||||
# Returns an ActiveRecord::Relation.
|
||||
def search(query)
|
||||
t = arel_table
|
||||
pattern = "%#{query}%"
|
||||
|
||||
where(t[:title].matches(pattern).or(t[:file_name].matches(pattern)))
|
||||
fuzzy_search(query, [:title, :file_name])
|
||||
end
|
||||
|
||||
# Searches for snippets with matching content.
|
||||
|
|
@ -149,10 +147,7 @@ class Snippet < ActiveRecord::Base
|
|||
#
|
||||
# Returns an ActiveRecord::Relation.
|
||||
def search_code(query)
|
||||
table = Snippet.arel_table
|
||||
pattern = "%#{query}%"
|
||||
|
||||
where(table[:content].matches(pattern))
|
||||
fuzzy_search(query, [:content])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ module Storage
|
|||
delegate :gitlab_shell, :repository_storage_path, to: :project
|
||||
|
||||
ROOT_PATH_PREFIX = '@hashed'.freeze
|
||||
STORAGE_VERSION = 1
|
||||
|
||||
def initialize(project)
|
||||
@project = project
|
||||
|
|
|
|||
|
|
@ -313,9 +313,6 @@ class User < ActiveRecord::Base
|
|||
#
|
||||
# Returns an ActiveRecord::Relation.
|
||||
def search(query)
|
||||
table = arel_table
|
||||
pattern = User.to_pattern(query)
|
||||
|
||||
order = <<~SQL
|
||||
CASE
|
||||
WHEN users.name = %{query} THEN 0
|
||||
|
|
@ -325,11 +322,8 @@ class User < ActiveRecord::Base
|
|||
END
|
||||
SQL
|
||||
|
||||
where(
|
||||
table[:name].matches(pattern)
|
||||
.or(table[:email].matches(pattern))
|
||||
.or(table[:username].matches(pattern))
|
||||
).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
|
||||
fuzzy_search(query, [:name, :email, :username])
|
||||
.reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
|
||||
end
|
||||
|
||||
# searches user by given pattern
|
||||
|
|
@ -337,16 +331,16 @@ class User < ActiveRecord::Base
|
|||
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
|
||||
|
||||
def search_with_secondary_emails(query)
|
||||
table = arel_table
|
||||
email_table = Email.arel_table
|
||||
pattern = "%#{query}%"
|
||||
matched_by_emails_user_ids = email_table.project(email_table[:user_id]).where(email_table[:email].matches(pattern))
|
||||
matched_by_emails_user_ids = email_table
|
||||
.project(email_table[:user_id])
|
||||
.where(Email.fuzzy_arel_match(:email, query))
|
||||
|
||||
where(
|
||||
table[:name].matches(pattern)
|
||||
.or(table[:email].matches(pattern))
|
||||
.or(table[:username].matches(pattern))
|
||||
.or(table[:id].in(matched_by_emails_user_ids))
|
||||
fuzzy_arel_match(:name, query)
|
||||
.or(fuzzy_arel_match(:email, query))
|
||||
.or(fuzzy_arel_match(:username, query))
|
||||
.or(arel_table[:id].in(matched_by_emails_user_ids))
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
require 'securerandom'
|
||||
|
||||
# Compare 2 branches for one repo or between repositories
|
||||
# Compare 2 refs for one repo or between repositories
|
||||
# and return Gitlab::Git::Compare object that responds to commits and diffs
|
||||
class CompareService
|
||||
attr_reader :start_project, :start_branch_name
|
||||
attr_reader :start_project, :start_ref_name
|
||||
|
||||
def initialize(new_start_project, new_start_branch_name)
|
||||
def initialize(new_start_project, new_start_ref_name)
|
||||
@start_project = new_start_project
|
||||
@start_branch_name = new_start_branch_name
|
||||
@start_ref_name = new_start_ref_name
|
||||
end
|
||||
|
||||
def execute(target_project, target_branch, straight: false)
|
||||
raw_compare = target_project.repository.compare_source_branch(target_branch, start_project.repository, start_branch_name, straight: straight)
|
||||
def execute(target_project, target_ref, straight: false)
|
||||
raw_compare = target_project.repository.compare_source_branch(target_ref, start_project.repository, start_ref_name, straight: straight)
|
||||
|
||||
Compare.new(raw_compare, target_project, straight: straight) if raw_compare
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
module MergeRequests
|
||||
class BuildService < MergeRequests::BaseService
|
||||
def execute
|
||||
@issue_iid = params.delete(:issue_iid)
|
||||
|
||||
self.merge_request = MergeRequest.new(params)
|
||||
merge_request.compare_commits = []
|
||||
merge_request.source_project = find_source_project
|
||||
|
|
@ -18,7 +20,17 @@ module MergeRequests
|
|||
|
||||
attr_accessor :merge_request
|
||||
|
||||
delegate :target_branch, :source_branch, :source_project, :target_project, :compare_commits, :wip_title, :description, :errors, to: :merge_request
|
||||
delegate :target_branch,
|
||||
:target_branch_ref,
|
||||
:target_project,
|
||||
:source_branch,
|
||||
:source_branch_ref,
|
||||
:source_project,
|
||||
:compare_commits,
|
||||
:wip_title,
|
||||
:description,
|
||||
:errors,
|
||||
to: :merge_request
|
||||
|
||||
def find_source_project
|
||||
return source_project if source_project.present? && can?(current_user, :read_project, source_project)
|
||||
|
|
@ -54,10 +66,10 @@ module MergeRequests
|
|||
def compare_branches
|
||||
compare = CompareService.new(
|
||||
source_project,
|
||||
source_branch
|
||||
source_branch_ref
|
||||
).execute(
|
||||
target_project,
|
||||
target_branch
|
||||
target_branch_ref
|
||||
)
|
||||
|
||||
if compare
|
||||
|
|
@ -106,37 +118,53 @@ module MergeRequests
|
|||
# more than one commit in the MR
|
||||
#
|
||||
def assign_title_and_description
|
||||
if match = source_branch.match(/\A(\d+)-/)
|
||||
iid = match[1]
|
||||
end
|
||||
assign_title_and_description_from_single_commit
|
||||
assign_title_from_issue
|
||||
|
||||
commits = compare_commits
|
||||
if commits && commits.count == 1
|
||||
commit = commits.first
|
||||
merge_request.title = commit.title
|
||||
merge_request.description ||= commit.description.try(:strip)
|
||||
elsif iid && issue = target_project.get_issue(iid, current_user)
|
||||
case issue
|
||||
when Issue
|
||||
merge_request.title = "Resolve \"#{issue.title}\""
|
||||
when ExternalIssue
|
||||
merge_request.title = "Resolve #{issue.title}"
|
||||
end
|
||||
merge_request.title ||= source_branch.titleize.humanize
|
||||
merge_request.title = wip_title if compare_commits.empty?
|
||||
|
||||
append_closes_description
|
||||
end
|
||||
|
||||
def append_closes_description
|
||||
return unless issue_iid
|
||||
|
||||
closes_issue = "Closes ##{issue_iid}"
|
||||
|
||||
if description.present?
|
||||
merge_request.description += closes_issue.prepend("\n\n")
|
||||
else
|
||||
merge_request.title = source_branch.titleize.humanize
|
||||
merge_request.description = closes_issue
|
||||
end
|
||||
end
|
||||
|
||||
if iid
|
||||
closes_issue = "Closes ##{iid}"
|
||||
def assign_title_and_description_from_single_commit
|
||||
commits = compare_commits
|
||||
|
||||
if description.present?
|
||||
merge_request.description += closes_issue.prepend("\n\n")
|
||||
else
|
||||
merge_request.description = closes_issue
|
||||
return unless commits&.count == 1
|
||||
|
||||
commit = commits.first
|
||||
merge_request.title ||= commit.title
|
||||
merge_request.description ||= commit.description.try(:strip)
|
||||
end
|
||||
|
||||
def assign_title_from_issue
|
||||
return unless issue
|
||||
|
||||
merge_request.title =
|
||||
case issue
|
||||
when Issue then "Resolve \"#{issue.title}\""
|
||||
when ExternalIssue then "Resolve #{issue.title}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
merge_request.title = wip_title if commits.empty?
|
||||
def issue_iid
|
||||
@issue_iid ||= source_branch.match(/\A(\d+)-/).try(:[], 1)
|
||||
end
|
||||
|
||||
def issue
|
||||
@issue ||= target_project.get_issue(issue_iid, current_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
module MergeRequests
|
||||
class CreateFromIssueService < MergeRequests::CreateService
|
||||
def initialize(project, user, params)
|
||||
# branch - the name of new branch
|
||||
# ref - the source of new branch.
|
||||
|
||||
@branch_name = params[:branch_name]
|
||||
@issue_iid = params[:issue_iid]
|
||||
@ref = params[:ref]
|
||||
|
||||
super(project, user)
|
||||
end
|
||||
|
||||
def execute
|
||||
return error('Invalid issue iid') unless issue_iid.present? && issue.present?
|
||||
return error('Invalid issue iid') unless @issue_iid.present? && issue.present?
|
||||
|
||||
params[:label_ids] = issue.label_ids if issue.label_ids.any?
|
||||
|
||||
|
|
@ -21,20 +32,16 @@ module MergeRequests
|
|||
|
||||
private
|
||||
|
||||
def issue_iid
|
||||
@isssue_iid ||= params.delete(:issue_iid)
|
||||
end
|
||||
|
||||
def issue
|
||||
@issue ||= IssuesFinder.new(current_user, project_id: project.id).find_by(iid: issue_iid)
|
||||
@issue ||= IssuesFinder.new(current_user, project_id: project.id).find_by(iid: @issue_iid)
|
||||
end
|
||||
|
||||
def branch_name
|
||||
@branch_name ||= issue.to_branch_name
|
||||
@branch ||= @branch_name || issue.to_branch_name
|
||||
end
|
||||
|
||||
def ref
|
||||
project.default_branch || 'master'
|
||||
@ref || project.default_branch || 'master'
|
||||
end
|
||||
|
||||
def merge_request
|
||||
|
|
@ -43,6 +50,7 @@ module MergeRequests
|
|||
|
||||
def merge_request_params
|
||||
{
|
||||
issue_iid: @issue_iid,
|
||||
source_project_id: project.id,
|
||||
source_branch: branch_name,
|
||||
target_project_id: project.id,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
module Projects
|
||||
module HashedStorage
|
||||
AttachmentMigrationError = Class.new(StandardError)
|
||||
|
||||
class MigrateAttachmentsService < BaseService
|
||||
attr_reader :logger, :old_path, :new_path
|
||||
|
||||
def initialize(project, logger = nil)
|
||||
@project = project
|
||||
@logger = logger || Rails.logger
|
||||
end
|
||||
|
||||
def execute
|
||||
@old_path = project.full_path
|
||||
@new_path = project.disk_path
|
||||
|
||||
origin = FileUploader.dynamic_path_segment(project)
|
||||
project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:attachments]
|
||||
target = FileUploader.dynamic_path_segment(project)
|
||||
|
||||
result = move_folder!(origin, target)
|
||||
project.save!
|
||||
|
||||
if result && block_given?
|
||||
yield
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def move_folder!(old_path, new_path)
|
||||
unless File.directory?(old_path)
|
||||
logger.info("Skipped attachments migration from '#{old_path}' to '#{new_path}', source path doesn't exist or is not a directory (PROJECT_ID=#{project.id})")
|
||||
return
|
||||
end
|
||||
|
||||
if File.exist?(new_path)
|
||||
logger.error("Cannot migrate attachments from '#{old_path}' to '#{new_path}', target path already exist (PROJECT_ID=#{project.id})")
|
||||
raise AttachmentMigrationError, "Target path '#{new_path}' already exist"
|
||||
end
|
||||
|
||||
# Create hashed storage base path folder
|
||||
FileUtils.mkdir_p(File.dirname(new_path))
|
||||
|
||||
FileUtils.mv(old_path, new_path)
|
||||
logger.info("Migrated project attachments from '#{old_path}' to '#{new_path}' (PROJECT_ID=#{project.id})")
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
module Projects
|
||||
module HashedStorage
|
||||
class MigrateRepositoryService < BaseService
|
||||
include Gitlab::ShellAdapter
|
||||
|
||||
attr_reader :old_disk_path, :new_disk_path, :old_wiki_disk_path, :old_storage_version, :logger
|
||||
|
||||
def initialize(project, logger = nil)
|
||||
@project = project
|
||||
@logger = logger || Rails.logger
|
||||
end
|
||||
|
||||
def execute
|
||||
@old_disk_path = project.disk_path
|
||||
has_wiki = project.wiki.repository_exists?
|
||||
|
||||
@old_storage_version = project.storage_version
|
||||
project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository]
|
||||
project.ensure_storage_path_exists
|
||||
|
||||
@new_disk_path = project.disk_path
|
||||
|
||||
result = move_repository(@old_disk_path, @new_disk_path)
|
||||
|
||||
if has_wiki
|
||||
@old_wiki_disk_path = "#{@old_disk_path}.wiki"
|
||||
result &&= move_repository("#{@old_wiki_disk_path}", "#{@new_disk_path}.wiki")
|
||||
end
|
||||
|
||||
unless result
|
||||
rollback_folder_move
|
||||
project.storage_version = nil
|
||||
end
|
||||
|
||||
project.repository_read_only = false
|
||||
project.save!
|
||||
|
||||
if result && block_given?
|
||||
yield
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def move_repository(from_name, to_name)
|
||||
from_exists = gitlab_shell.exists?(project.repository_storage_path, "#{from_name}.git")
|
||||
to_exists = gitlab_shell.exists?(project.repository_storage_path, "#{to_name}.git")
|
||||
|
||||
# If we don't find the repository on either original or target we should log that as it could be an issue if the
|
||||
# project was not originally empty.
|
||||
if !from_exists && !to_exists
|
||||
logger.warn "Can't find a repository on either source or target paths for #{project.full_path} (ID=#{project.id}) ..."
|
||||
return false
|
||||
elsif !from_exists
|
||||
# Repository have been moved already.
|
||||
return true
|
||||
end
|
||||
|
||||
gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name)
|
||||
end
|
||||
|
||||
def rollback_folder_move
|
||||
move_repository(@new_disk_path, @old_disk_path)
|
||||
move_repository("#{@new_disk_path}.wiki", "#{@old_disk_path}.wiki")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,68 +1,22 @@
|
|||
module Projects
|
||||
class HashedStorageMigrationService < BaseService
|
||||
include Gitlab::ShellAdapter
|
||||
|
||||
attr_reader :old_disk_path, :new_disk_path
|
||||
attr_reader :logger
|
||||
|
||||
def initialize(project, logger = nil)
|
||||
@project = project
|
||||
@logger ||= Rails.logger
|
||||
@logger = logger || Rails.logger
|
||||
end
|
||||
|
||||
def execute
|
||||
return if project.hashed_storage?(:repository)
|
||||
|
||||
@old_disk_path = project.disk_path
|
||||
has_wiki = project.wiki.repository_exists?
|
||||
|
||||
project.storage_version = Storage::HashedProject::STORAGE_VERSION
|
||||
project.ensure_storage_path_exists
|
||||
|
||||
@new_disk_path = project.disk_path
|
||||
|
||||
result = move_repository(@old_disk_path, @new_disk_path)
|
||||
|
||||
if has_wiki
|
||||
result &&= move_repository("#{@old_disk_path}.wiki", "#{@new_disk_path}.wiki")
|
||||
# Migrate repository from Legacy to Hashed Storage
|
||||
unless project.hashed_storage?(:repository)
|
||||
return unless HashedStorage::MigrateRepositoryService.new(project, logger).execute
|
||||
end
|
||||
|
||||
unless result
|
||||
rollback_folder_move
|
||||
return
|
||||
# Migrate attachments from Legacy to Hashed Storage
|
||||
unless project.hashed_storage?(:attachments)
|
||||
HashedStorage::MigrateAttachmentsService.new(project, logger).execute
|
||||
end
|
||||
|
||||
project.repository_read_only = false
|
||||
project.save!
|
||||
|
||||
block_given? ? yield : result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def move_repository(from_name, to_name)
|
||||
from_exists = gitlab_shell.exists?(project.repository_storage_path, "#{from_name}.git")
|
||||
to_exists = gitlab_shell.exists?(project.repository_storage_path, "#{to_name}.git")
|
||||
|
||||
# If we don't find the repository on either original or target we should log that as it could be an issue if the
|
||||
# project was not originally empty.
|
||||
if !from_exists && !to_exists
|
||||
logger.warn "Can't find a repository on either source or target paths for #{project.full_path} (ID=#{project.id}) ..."
|
||||
return false
|
||||
elsif !from_exists
|
||||
# Repository have been moved already.
|
||||
return true
|
||||
end
|
||||
|
||||
gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name)
|
||||
end
|
||||
|
||||
def rollback_folder_move
|
||||
move_repository(@new_disk_path, @old_disk_path)
|
||||
move_repository("#{@new_disk_path}.wiki", "#{@old_disk_path}.wiki")
|
||||
end
|
||||
|
||||
def logger
|
||||
@logger
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ module Projects
|
|||
return error("Could not set the default branch") unless project.change_head(params[:default_branch])
|
||||
end
|
||||
|
||||
if project.update_attributes(params.except(:default_branch))
|
||||
if project.update_attributes(update_params)
|
||||
if project.previous_changes.include?('path')
|
||||
project.rename_repo
|
||||
else
|
||||
|
|
@ -31,8 +31,16 @@ module Projects
|
|||
end
|
||||
end
|
||||
|
||||
def run_auto_devops_pipeline?
|
||||
params.dig(:run_auto_devops_pipeline_explicit) == 'true' || params.dig(:run_auto_devops_pipeline_implicit) == 'true'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_params
|
||||
params.except(:default_branch, :run_auto_devops_pipeline_explicit, :run_auto_devops_pipeline_implicit)
|
||||
end
|
||||
|
||||
def renaming_project_with_container_registry_tags?
|
||||
new_path = params[:path]
|
||||
|
||||
|
|
|
|||
|
|
@ -31,12 +31,19 @@ class FileUploader < GitlabUploader
|
|||
# Returns a String without a trailing slash
|
||||
def self.dynamic_path_segment(project)
|
||||
if project.hashed_storage?(:attachments)
|
||||
File.join(CarrierWave.root, base_dir, project.disk_path)
|
||||
dynamic_path_builder(project.disk_path)
|
||||
else
|
||||
File.join(CarrierWave.root, base_dir, project.full_path)
|
||||
dynamic_path_builder(project.full_path)
|
||||
end
|
||||
end
|
||||
|
||||
# Auxiliary method to build dynamic path segment when not using a project model
|
||||
#
|
||||
# Prefer to use the `.dynamic_path_segment` as it includes Hashed Storage specific logic
|
||||
def self.dynamic_path_builder(path)
|
||||
File.join(CarrierWave.root, base_dir, path)
|
||||
end
|
||||
|
||||
attr_accessor :model
|
||||
attr_reader :secret
|
||||
|
||||
|
|
|
|||
|
|
@ -731,6 +731,30 @@
|
|||
.help-block
|
||||
Number of Git pushes after which 'git gc' is run.
|
||||
|
||||
%fieldset
|
||||
%legend Gitaly Timeouts
|
||||
.form-group
|
||||
= f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'control-label col-sm-2'
|
||||
.col-sm-10
|
||||
= f.number_field :gitaly_timeout_default, class: 'form-control'
|
||||
.help-block
|
||||
Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced
|
||||
for git fetch/push operations or Sidekiq jobs.
|
||||
.form-group
|
||||
= f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'control-label col-sm-2'
|
||||
.col-sm-10
|
||||
= f.number_field :gitaly_timeout_fast, class: 'form-control'
|
||||
.help-block
|
||||
Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast.
|
||||
If they exceed this threshold, there may be a problem with a storage shard and 'failing fast'
|
||||
can help maintain the stability of the GitLab instance.
|
||||
.form-group
|
||||
= f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'control-label col-sm-2'
|
||||
.col-sm-10
|
||||
= f.number_field :gitaly_timeout_medium, class: 'form-control'
|
||||
.help-block
|
||||
Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout.
|
||||
|
||||
%fieldset
|
||||
%legend Web terminal
|
||||
.form-group
|
||||
|
|
|
|||
|
|
@ -5,7 +5,12 @@ xml.entry do
|
|||
xml.link href: event_feed_url(event)
|
||||
xml.title truncate(event_feed_title(event), length: 80)
|
||||
xml.updated event.updated_at.xmlschema
|
||||
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author_email))
|
||||
|
||||
# We're deliberately re-using "event.author" here since this data is
|
||||
# eager-loaded. This allows us to re-use the user object's Email address,
|
||||
# instead of having to run additional queries to figure out what Email to use
|
||||
# for the avatar.
|
||||
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(event.author))
|
||||
|
||||
xml.author do
|
||||
xml.username event.author_username
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
.flash-container.flash-container-page
|
||||
- if alert
|
||||
.flash-alert
|
||||
%div{ class: (container_class) }
|
||||
%span= alert
|
||||
|
||||
- elsif notice
|
||||
.flash-notice
|
||||
%div{ class: (container_class) }
|
||||
%span= notice
|
||||
-# We currently only support `alert`, `notice`, `success`
|
||||
- flash.each do |key, value|
|
||||
-# Don't show a flash message if the message is nil
|
||||
- if value
|
||||
%div{ class: "flash-#{key}" }
|
||||
%div{ class: (container_class) }
|
||||
%span= value
|
||||
|
|
|
|||
|
|
@ -67,8 +67,8 @@
|
|||
- if @commit.last_pipeline
|
||||
- last_pipeline = @commit.last_pipeline
|
||||
.well-segment.pipeline-info
|
||||
.status-icon-container{ class: "ci-status-icon-#{last_pipeline.status}" }
|
||||
= link_to project_pipeline_path(@project, last_pipeline.id) do
|
||||
.status-icon-container
|
||||
= link_to project_pipeline_path(@project, last_pipeline.id), class: "ci-status-icon-#{last_pipeline.status}" do
|
||||
= ci_icon_for_status(last_pipeline.status)
|
||||
#{ _('Pipeline') }
|
||||
= link_to "##{last_pipeline.id}", project_pipeline_path(@project, last_pipeline.id)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
- avatar = namespace_icon(namespace, 100)
|
||||
- can_create_project = current_user.can?(:create_projects, namespace)
|
||||
|
||||
- if forked_project = namespace.find_fork_of(@project)
|
||||
.bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default.forked
|
||||
= link_to project_path(forked_project) do
|
||||
- if /no_((\w*)_)*avatar/.match(avatar)
|
||||
= project_identicon(namespace, class: "avatar s100 identicon")
|
||||
- else
|
||||
.avatar-container.s100
|
||||
= image_tag(avatar, class: "avatar s100")
|
||||
%h5.prepend-top-default
|
||||
= namespace.human_name
|
||||
- else
|
||||
.bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default{ class: ("disabled" unless can_create_project) }
|
||||
= link_to project_forks_path(@project, namespace_key: namespace.id),
|
||||
method: "POST",
|
||||
class: ("disabled has-tooltip" unless can_create_project),
|
||||
title: (_('You have reached your project limit') unless can_create_project) do
|
||||
- if /no_((\w*)_)*avatar/.match(avatar)
|
||||
= project_identicon(namespace, class: "avatar s100 identicon")
|
||||
- else
|
||||
.avatar-container.s100
|
||||
= image_tag(avatar, class: "avatar s100")
|
||||
%h5.prepend-top-default
|
||||
= namespace.human_name
|
||||
|
|
@ -14,22 +14,7 @@
|
|||
%h5.prepend-top-0.append-bottom-0.prepend-left-default.append-right-default
|
||||
Click to fork the project
|
||||
- @namespaces.each do |namespace|
|
||||
- avatar = namespace_icon(namespace, 100)
|
||||
- can_create_project = current_user.can?(:create_projects, namespace)
|
||||
- forked_project = namespace.find_fork_of(@project)
|
||||
- fork_path = forked_project ? project_path(forked_project) : project_forks_path(@project, namespace_key: namespace.id)
|
||||
.bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default{ class: [("disabled" unless can_create_project), ("forked" if forked_project)] }
|
||||
= link_to fork_path,
|
||||
method: "POST",
|
||||
class: [("js-fork-thumbnail" unless forked_project), ("disabled has-tooltip" unless can_create_project)],
|
||||
title: (_('You have reached your project limit') unless can_create_project) do
|
||||
- if /no_((\w*)_)*avatar/.match(avatar)
|
||||
= project_identicon(namespace, class: "avatar s100 identicon")
|
||||
- else
|
||||
.avatar-container.s100
|
||||
= image_tag(avatar, class: "avatar s100")
|
||||
%h5.prepend-top-default
|
||||
= namespace.human_name
|
||||
= render 'fork_button', namespace: namespace
|
||||
- else
|
||||
%strong
|
||||
No available namespaces to fork the project.
|
||||
|
|
|
|||
|
|
@ -1,34 +1,50 @@
|
|||
- can_create_merge_request = can?(current_user, :create_merge_request, @project)
|
||||
- data_action = can_create_merge_request ? 'create-mr' : 'create-branch'
|
||||
- value = can_create_merge_request ? 'Create a merge request' : 'Create a branch'
|
||||
- value = can_create_merge_request ? 'Create merge request' : 'Create branch'
|
||||
|
||||
- if can?(current_user, :push_code, @project)
|
||||
.create-mr-dropdown-wrap{ data: { can_create_path: can_create_branch_project_issue_path(@project, @issue), create_mr_path: create_merge_request_project_issue_path(@project, @issue), create_branch_path: project_branches_path(@project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid) } }
|
||||
- can_create_path = can_create_branch_project_issue_path(@project, @issue)
|
||||
- create_mr_path = create_merge_request_project_issue_path(@project, @issue, branch_name: @issue.to_branch_name, ref: @project.default_branch)
|
||||
- create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid)
|
||||
- refs_path = refs_namespace_project_path(@project.namespace, @project, search: '')
|
||||
|
||||
.create-mr-dropdown-wrap{ data: { can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path } }
|
||||
.btn-group.unavailable
|
||||
%button.btn.btn-grouped{ type: 'button', disabled: 'disabled' }
|
||||
= icon('spinner', class: 'fa-spin')
|
||||
%span.text
|
||||
Checking branch availability…
|
||||
.btn-group.available.hide
|
||||
%input.btn.js-create-merge-request.btn-inverted.btn-success{ type: 'button', value: value, data: { action: data_action } }
|
||||
%button.btn.btn-inverted.dropdown-toggle.btn-inverted.btn-success.js-dropdown-toggle{ type: 'button', data: { 'dropdown-trigger' => '#create-merge-request-dropdown' } }
|
||||
%button.btn.js-create-merge-request.btn-default{ type: 'button', data: { action: data_action } }
|
||||
= value
|
||||
|
||||
%button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-default.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' } } }
|
||||
= icon('caret-down')
|
||||
%ul#create-merge-request-dropdown.dropdown-menu.dropdown-menu-align-right{ data: { dropdown: true } }
|
||||
|
||||
%ul#create-merge-request-dropdown.create-merge-request-dropdown-menu.dropdown-menu.dropdown-menu-align-right.gl-show-field-errors{ data: { dropdown: true } }
|
||||
- if can_create_merge_request
|
||||
%li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', 'text' => 'Create a merge request' } }
|
||||
.menu-item
|
||||
.icon-container
|
||||
= icon('check')
|
||||
.description
|
||||
%strong Create a merge request
|
||||
%span
|
||||
Creates a merge request named after this issue, with source branch created from '#{@project.default_branch}'.
|
||||
%li.divider.droplab-item-ignore
|
||||
%li{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', 'text' => 'Create a branch' } }
|
||||
.menu-item
|
||||
.icon-container
|
||||
= icon('check')
|
||||
.description
|
||||
%strong Create a branch
|
||||
%span
|
||||
Creates a branch named after this issue, from '#{@project.default_branch}'.
|
||||
%li.create-item.droplab-item-selected.droplab-item-ignore-hiding{ role: 'button', data: { value: 'create-mr', text: 'Create merge request' } }
|
||||
.menu-item.droplab-item-ignore-hiding
|
||||
.icon-container.droplab-item-ignore-hiding= icon('check')
|
||||
.description.droplab-item-ignore-hiding Create merge request and branch
|
||||
|
||||
%li.create-item.droplab-item-ignore-hiding{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', text: 'Create branch' } }
|
||||
.menu-item.droplab-item-ignore-hiding
|
||||
.icon-container.droplab-item-ignore-hiding= icon('check')
|
||||
.description.droplab-item-ignore-hiding Create branch
|
||||
%li.divider
|
||||
|
||||
%li.droplab-item-ignore
|
||||
Branch name
|
||||
%input.js-branch-name.form-control.droplab-item-ignore{ type: 'text', placeholder: "#{@issue.to_branch_name}", value: "#{@issue.to_branch_name}" }
|
||||
%span.js-branch-message.branch-message.droplab-item-ignore
|
||||
|
||||
%li.droplab-item-ignore
|
||||
Source (branch or tag)
|
||||
%input.js-ref.ref.form-control.droplab-item-ignore{ type: 'text', placeholder: "#{@project.default_branch}", value: "#{@project.default_branch}", data: { value: "#{@project.default_branch}" } }
|
||||
%span.js-ref-message.ref-message.droplab-item-ignore
|
||||
|
||||
%li.droplab-item-ignore
|
||||
%button.btn.btn-success.js-create-target.droplab-item-ignore{ type: 'button', data: { action: 'create-mr' } }
|
||||
Create merge request
|
||||
|
||||
|
|
|
|||
|
|
@ -13,29 +13,39 @@
|
|||
%p.settings-message.text-center
|
||||
= message.html_safe
|
||||
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
|
||||
.radio
|
||||
.radio.js-auto-devops-enable-radio-wrapper
|
||||
= form.label :enabled_true do
|
||||
= form.radio_button :enabled, 'true'
|
||||
= form.radio_button :enabled, 'true', class: 'js-auto-devops-enable-radio'
|
||||
%strong Enable Auto DevOps
|
||||
%br
|
||||
%span.descr
|
||||
The Auto DevOps pipeline configuration will be used when there is no <code>.gitlab-ci.yml</code> in the project.
|
||||
.radio
|
||||
- if show_run_auto_devops_pipeline_checkbox_for_explicit_setting?(@project)
|
||||
.checkbox.hide.js-run-auto-devops-pipeline-checkbox-wrapper
|
||||
= label_tag 'project[run_auto_devops_pipeline_explicit]' do
|
||||
= check_box_tag 'project[run_auto_devops_pipeline_explicit]', true, false, class: 'js-run-auto-devops-pipeline-checkbox'
|
||||
= s_('ProjectSettings|Immediately run a pipeline on the default branch')
|
||||
|
||||
.radio.js-auto-devops-enable-radio-wrapper
|
||||
= form.label :enabled_false do
|
||||
= form.radio_button :enabled, 'false'
|
||||
= form.radio_button :enabled, 'false', class: 'js-auto-devops-enable-radio'
|
||||
%strong Disable Auto DevOps
|
||||
%br
|
||||
%span.descr
|
||||
An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continuous Integration and Delivery.
|
||||
|
||||
.radio
|
||||
= form.label :enabled_nil do
|
||||
= form.radio_button :enabled, ''
|
||||
.radio.js-auto-devops-enable-radio-wrapper
|
||||
= form.label :enabled_ do
|
||||
= form.radio_button :enabled, '', class: 'js-auto-devops-enable-radio'
|
||||
%strong Instance default (#{current_application_settings.auto_devops_enabled? ? 'enabled' : 'disabled'})
|
||||
%br
|
||||
%span.descr
|
||||
Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific <code>.gitlab-ci.yml</code>.
|
||||
%br
|
||||
- if show_run_auto_devops_pipeline_checkbox_for_instance_setting?(@project)
|
||||
.checkbox.hide.js-run-auto-devops-pipeline-checkbox-wrapper
|
||||
= label_tag 'project[run_auto_devops_pipeline_implicit]' do
|
||||
= check_box_tag 'project[run_auto_devops_pipeline_implicit]', true, false, class: 'js-run-auto-devops-pipeline-checkbox'
|
||||
= s_('ProjectSettings|Immediately run a pipeline on the default branch')
|
||||
%p
|
||||
You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.
|
||||
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
|
||||
|
|
|
|||
|
|
@ -11,6 +11,6 @@
|
|||
= webpack_bundle_tag 'common_vue'
|
||||
= webpack_bundle_tag 'repo'
|
||||
|
||||
%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
|
||||
%div{ class: [(container_class unless show_new_repo?), ("limit-container-width" unless fluid_layout)] }
|
||||
= render 'projects/last_push'
|
||||
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
%span
|
||||
= enabled_project_button(project, enabled_protocol)
|
||||
- else
|
||||
%a#clone-dropdown.clone-dropdown-btn.btn{ href: '#', data: { toggle: 'dropdown' } }
|
||||
%a#clone-dropdown.btn.clone-dropdown-btn{ href: '#', data: { toggle: 'dropdown' } }
|
||||
%span
|
||||
= default_clone_protocol.upcase
|
||||
= icon('caret-down')
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
= project_icon(project, alt: '', class: 'avatar project-avatar s40')
|
||||
.project-details
|
||||
%h3.prepend-top-0.append-bottom-0
|
||||
= link_to project_path(project), class: dom_class(project) do
|
||||
= link_to project_path(project), class: 'text-plain' do
|
||||
%span.project-full-name
|
||||
%span.namespace-name
|
||||
- if project.namespace && !skip_namespace
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
- @no_container = true;
|
||||
#repo{ data: { root: @path.empty?.to_s,
|
||||
root_url: project_tree_path(project),
|
||||
url: content_url,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
class CreatePipelineWorker
|
||||
include Sidekiq::Worker
|
||||
include PipelineQueue
|
||||
|
||||
enqueue_in group: :creation
|
||||
|
||||
def perform(project_id, user_id, ref, source, params = {})
|
||||
project = Project.find(project_id)
|
||||
user = User.find(user_id)
|
||||
params = params.deep_symbolize_keys
|
||||
|
||||
Ci::CreatePipelineService
|
||||
.new(project, user, ref: ref)
|
||||
.execute(source, **params)
|
||||
end
|
||||
end
|
||||
|
|
@ -2,10 +2,34 @@ class ProjectMigrateHashedStorageWorker
|
|||
include Sidekiq::Worker
|
||||
include DedicatedSidekiqQueue
|
||||
|
||||
LEASE_TIMEOUT = 30.seconds.to_i
|
||||
|
||||
def perform(project_id)
|
||||
project = Project.find_by(id: project_id)
|
||||
return if project.nil? || project.pending_delete?
|
||||
|
||||
::Projects::HashedStorageMigrationService.new(project, logger).execute
|
||||
uuid = lease_for(project_id).try_obtain
|
||||
if uuid
|
||||
::Projects::HashedStorageMigrationService.new(project, logger).execute
|
||||
else
|
||||
false
|
||||
end
|
||||
rescue => ex
|
||||
cancel_lease_for(project_id, uuid) if uuid
|
||||
raise ex
|
||||
end
|
||||
|
||||
def lease_for(project_id)
|
||||
Gitlab::ExclusiveLease.new(lease_key(project_id), timeout: LEASE_TIMEOUT)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def lease_key(project_id)
|
||||
"project_migrate_hashed_storage_worker:#{project_id}"
|
||||
end
|
||||
|
||||
def cancel_lease_for(project_id, uuid)
|
||||
Gitlab::ExclusiveLease.cancel(lease_key(project_id), uuid)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Initializes the branches dropdown when the 'Start new pipeline' failed due to validation errors
|
||||
merge_request: 15588
|
||||
author: Christiaan Van den Poel
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add an ability to use a custom branch name on creation from issues
|
||||
merge_request: 13884
|
||||
author: Vitaliy @blackst0ne Klachkov
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: added support for ordering and sorting in notes api
|
||||
merge_request: 15342
|
||||
author: haseebeqx
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add the option to automatically run a pipeline after updating AutoDevOps settings
|
||||
merge_request: 15380
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Removed tooltip from clone dropdown
|
||||
merge_request: 15334
|
||||
author:
|
||||
type: other
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue