Commit 386cbf22 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into multi-file-editor-dirty-diff-indicator

parents 1880809d 73e48b74
......@@ -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)
......
/* 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 'clipboard';
import Clipboard from 'vendor/clipboard';
function showTooltip(target, title) {
const $target = $(target);
const originalTitle = $target.data('original-title');
var genericError, genericSuccess, showTooltip;
if (!$target.data('hideTooltip')) {
$target
.attr('title', title)
.tooltip('fixTitle')
.tooltip('show')
.attr('title', originalTitle)
.tooltip('fixTitle');
}
}
genericSuccess = function(e) {
function genericSuccess(e) {
showTooltip(e.trigger, 'Copied');
// Clear the selection and blur the trigger so it loses its border
e.clearSelection();
return $(e.trigger).blur();
};
$(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;
/**
* 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';
}
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');
}
};
showTooltip(e.trigger, `Press ${key}-C to copy`);
}
$(function() {
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]', function(e) {
/**
* 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;
......@@ -71,4 +70,4 @@ $(function() {
clipboardData.setData('text/plain', json.text);
clipboardData.setData('text/x-gfm', json.gfm);
});
});
}
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();
......@@ -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,
};
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);
});
......
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>
......
<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>
<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>
<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>
<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>
</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>
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>
</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;
}
}
......
.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,273 +35,245 @@
}
}
.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;
&.is-collapsed {
.ide-file-list {
max-width: 250px;
}
}
}
.code.white pre .hll {
background-color: $well-light-border !important;
.ide-file-list {
flex: 1;
overflow: scroll;
.file {
cursor: pointer;
}
.tree-content-holder {
display: -webkit-flex;
display: flex;
min-height: 300px;
a {
color: $gl-text-color;
}
.tree-content-holder-mini {
height: 100vh;
th {
position: sticky;
top: 0;
}
}
.panel-right {
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
flex-direction: column;
width: 80%;
height: 100%;
.multi-file-table-name,
.multi-file-table-col-commit-message {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 0;
}
.monaco-editor.vs {
.current-line {
border: 0;
background: $well-light-border;
}
.multi-file-table-name {
width: 350px;
}
.line-numbers {
cursor: pointer;
min-width: initial;
.multi-file-table-col-commit-message {
width: 50%;
}
&:hover {
text-decoration: underline;
}
}
}
.multi-file-edit-pane {
display: flex;
flex-direction: column;
flex: 1;
border-left: 1px solid $white-dark;
overflow: hidden;
}
.blob-no-preview {
.vertical-center {
justify-content: center;
width: 100%;
}
}
.multi-file-tabs {
display: flex;
overflow: scroll;
background-color: $white-normal;
box-shadow: inset 0 -1px $white-dark;
&.blob-editor-container {
overflow: hidden;
}
> li {
position: relative;
}
}
.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;
}
}
.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;
}
}
#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;
}
}
}
.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;
}
.repo-file-buttons {
background-color: $white-light;
padding: 5px 10px;
border-top: 1px solid $white-normal;
}
&.modified {
color: $indigo-700;
}
}
#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;
}
}
}
.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%;
}
#commit-area {
background: $gray-light;
padding: 20px;
.file-content.code {
display: flex;
.help-block {
padding-top: 7px;
margin-top: 0;
i {
margin-left: -10px;
}
}
#view-toggler {
height: 41px;
position: relative;
display: block;
border-bottom: 1px solid $white-normal;
background: $white-light;
margin-top: -5px;
.line-numbers {
min-width: 50px;
}
#binary-viewer {
img {
max-width: 100%;
}
.file-content,
.line-numbers,
.blob-content,
.code {
min-height: 100%;
}
}
#sidebar {
flex: 1;
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;
}
}
&.sidebar-mini {
width: 20%;
border-right: 1px solid $white-normal;
overflow: auto;
}
.multi-file-commit-panel-section {
display: flex;
flex-direction: column;
flex: 1;
}
.table {
margin-bottom: 0;
}
.multi-file-commit-panel-header {
display: flex;
align-items: center;
padding: 0 0 12px;
margin-bottom: 12px;
border-bottom: 1px solid $white-dark;
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;
}
}
&.is-collapsed {
border-bottom: 1px solid $white-dark;
.file {
cursor: pointer;
svg {
margin-left: auto;
margin-right: auto;
}
}
}
a {
@include str-truncated(250px);
color: $almost-black;
}
.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;
}
}
.render-error {
min-height: calc(100vh - 62px);
.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;
p {
width: 100%;
.btn {
flex: 1;
}
}
.multi-file-table-col-name {
width: 350px;
.multi-file-commit-message.form-control {
height: 80px;
resize: none;
}
.dirty-diff {
......
......@@ -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
......
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
dropdown_item_with_description(protocol, dropdown_description, href: append_url)
end
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
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
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
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('SSH', 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 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 (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.')
}
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
......
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: [])
......
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
......
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
......
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
......
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
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
if description.present?
merge_request.description += closes_issue.prepend("\n\n")
else
merge_request.description = closes_issue
merge_request.title =
case issue
when Issue then "Resolve \"#{issue.title}\""
when ExternalIssue then "Resolve #{issue.title}"
end
end
end
def issue_iid
@issue_iid ||= source_branch.match(/\A(\d+)-/).try(:[], 1)
end
merge_request.title = wip_title if commits.empty?
def issue
@issue ||= target_project.get_issue(issue_iid, current_user)
end
end
end
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,
......
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
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
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")
end
unless result
rollback_folder_move
return
# Migrate repository from Legacy to Hashed Storage
unless project.hashed_storage?(:repository)
return unless HashedStorage::MigrateRepositoryService.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
# Migrate attachments from Legacy to Hashed Storage
unless project.hashed_storage?(:attachments)
HashedStorage::MigrateAttachmentsService.new(project, logger).execute
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
......
.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)
......
- 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.
......
- 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
......
- @no_container = true;
#repo{ data: { root: @path.empty?.to_s,
root_url: project_tree_path(project),
url: content_url,
......
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
---
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
---
title: Add an ability to use a custom branch name on creation from issues
merge_request: 13884
author: Vitaliy @blackst0ne Klachkov
type: added
---
title: added support for ordering and sorting in notes api
merge_request: 15342
author: haseebeqx
type: added
---
title: Add the option to automatically run a pipeline after updating AutoDevOps settings
merge_request: 15380
author:
type: changed
---
title: Removed tooltip from clone dropdown
merge_request: 15334
author:
type: other
---
title: Fix item name and namespace text overflow in Projects dropdown
merge_request: 15451
author:
type: fixed
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment