Commit c9ed0777 authored by Simon Knox's avatar Simon Knox

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ee into ee-psimyn-issue-note-refac

parents b6ea6f31 d15e704e
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
## 10.2.3 (2017-11-30)
### Fixed (5 changes)
- Fix viewing default push rules on a Geo secondary. !3559
- Disable autocomplete for epics.
- Fix epic fullscreen editing.
- Fix tasklist for epics.
- Fix Geo wiki sync error not increasing retry count.
## 10.2.2 (2017-11-23) ## 10.2.2 (2017-11-23)
### Fixed (6 changes) ### Fixed (6 changes)
......
...@@ -2,6 +2,25 @@ ...@@ -2,6 +2,25 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.2.3 (2017-11-30)
### Fixed (7 changes)
- Fix hashed storage for Import/Export uploads. !15482
- Ensure that rake gitlab:cleanup:repos task does not mess with hashed repositories. !15520
- Ensure that rake gitlab:cleanup:dirs task does not mess with hashed repositories. !15600
- Fix WIP system note not being created.
- Fix link text from group context.
- Fix defaults for MR states and merge statuses.
- Fix pulling and pushing using a personal access token with the sudo scope.
### Performance (3 changes)
- Drastically improve project search performance by no longer searching namespace name.
- Reuse authors when rendering event Atom feeds.
- Optimise StuckCiJobsWorker using cheap SQL query outside, and expensive inside.
## 10.2.2 (2017-11-23) ## 10.2.2 (2017-11-23)
### Fixed (5 changes) ### Fixed (5 changes)
......
...@@ -1191,7 +1191,7 @@ DEPENDENCIES ...@@ -1191,7 +1191,7 @@ DEPENDENCIES
sanitize (~> 2.0) sanitize (~> 2.0)
sass-rails (~> 5.0.6) sass-rails (~> 5.0.6)
scss_lint (~> 0.54.0) scss_lint (~> 0.54.0)
seed-fu (~> 2.3.5) seed-fu (~> 2.3.7)
select2-rails (~> 3.5.9) select2-rails (~> 3.5.9)
selenium-webdriver (~> 3.5) selenium-webdriver (~> 3.5)
sentry-raven (~> 2.5.3) 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 'clipboard';
var genericError, genericSuccess, showTooltip; 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');
}
}
genericSuccess = function(e) { function genericSuccess(e) {
showTooltip(e.trigger, 'Copied'); showTooltip(e.trigger, 'Copied');
// Clear the selection and blur the trigger so it loses its border // Clear the selection and blur the trigger so it loses its border
e.clearSelection(); e.clearSelection();
return $(e.trigger).blur(); $(e.trigger).blur();
}; }
// Safari doesn't support `execCommand`, so instead we inform the user to /**
// copy manually. * Safari > 10 doesn't support `execCommand`, so instead we inform the user to copy manually.
// * See http://clipboardjs.com/#browser-support
// See http://clipboardjs.com/#browser-support */
genericError = function(e) { function genericError(e) {
var key; let key;
if (/Mac/i.test(navigator.userAgent)) { if (/Mac/i.test(navigator.userAgent)) {
key = '⌘'; // Command key = '⌘'; // Command
} else { } else {
key = 'Ctrl'; key = 'Ctrl';
} }
return showTooltip(e.trigger, "Press " + key + "-C to copy"); 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() { export default function initCopyToClipboard() {
const clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]'); const clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
clipboard.on('success', genericSuccess); clipboard.on('success', genericSuccess);
clipboard.on('error', genericError); 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` * This a workaround around ClipboardJS limitations to allow the context-specific copy/pasting
// attribute that ClipboardJS reads from. * of plain text or GFM. The Ruby `clipboard_button` helper sneaks a JSON hash with `text` and
// When ClipboardJS creates a new `textarea` (directly inside `body`, with a `readonly` attribute`), sets its value * `gfm` keys into the `data-clipboard-text` attribute that ClipboardJS reads from.
// to the value of this data attribute, focusses on it, and finally programmatically issues the 'Copy' command, * When ClipboardJS creates a new `textarea` (directly inside `body`, with a `readonly`
// this code intercepts the copy command/event at the last minute to deconstruct this JSON hash and set the * attribute`), sets its value to the value of this data attribute, focusses on it, and finally
// `text/plain` and `text/x-gfm` copy data types to the intended values. * programmatically issues the 'Copy' command, this code intercepts the copy command/event at
$(document).on('copy', 'body > textarea[readonly]', function(e) { * 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; const clipboardData = e.originalEvent.clipboardData;
if (!clipboardData) return; if (!clipboardData) return;
...@@ -71,4 +70,4 @@ $(function() { ...@@ -71,4 +70,4 @@ $(function() {
clipboardData.setData('text/plain', json.text); clipboardData.setData('text/plain', json.text);
clipboardData.setData('text/x-gfm', json.gfm); clipboardData.setData('text/x-gfm', json.gfm);
}); });
}); }
import './autosize'; import './autosize';
import './bind_in_out'; import './bind_in_out';
import initCopyAsGFM from './copy_as_gfm'; import initCopyAsGFM from './copy_as_gfm';
import initCopyToClipboard from './copy_to_clipboard';
import './details_behavior'; import './details_behavior';
import installGlEmojiElement from './gl_emoji'; import installGlEmojiElement from './gl_emoji';
import './quick_submit'; import './quick_submit';
...@@ -9,3 +10,4 @@ import './toggler_behavior'; ...@@ -9,3 +10,4 @@ import './toggler_behavior';
installGlEmojiElement(); installGlEmojiElement();
initCopyAsGFM(); initCopyAsGFM();
initCopyToClipboard();
...@@ -424,6 +424,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -424,6 +424,7 @@ import initGroupAnalytics from './init_group_analytics';
projectImport(); projectImport();
break; break;
case 'projects:pipelines:new': case 'projects:pipelines:new':
case 'projects:pipelines:create':
new NewBranchForm($('.js-new-pipeline-form')); new NewBranchForm($('.js-new-pipeline-form'));
break; break;
case 'projects:pipelines:builds': case 'projects:pipelines:builds':
...@@ -583,6 +584,13 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -583,6 +584,13 @@ import initGroupAnalytics from './init_group_analytics';
case 'projects:settings:ci_cd:show': case 'projects:settings:ci_cd:show':
// Initialize expandable settings panels // Initialize expandable settings panels
initSettingsPanels(); 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': case 'groups:settings:ci_cd:show':
new ProjectVariables(); new ProjectVariables();
break; break;
......
...@@ -3,6 +3,7 @@ const DATA_DROPDOWN = 'data-dropdown'; ...@@ -3,6 +3,7 @@ const DATA_DROPDOWN = 'data-dropdown';
const SELECTED_CLASS = 'droplab-item-selected'; const SELECTED_CLASS = 'droplab-item-selected';
const ACTIVE_CLASS = 'droplab-item-active'; const ACTIVE_CLASS = 'droplab-item-active';
const IGNORE_CLASS = 'droplab-item-ignore'; const IGNORE_CLASS = 'droplab-item-ignore';
const IGNORE_HIDING_CLASS = 'droplab-item-ignore-hiding';
// Matches `{{anything}}` and `{{ everything }}`. // Matches `{{anything}}` and `{{ everything }}`.
const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g; const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g;
...@@ -13,4 +14,5 @@ export { ...@@ -13,4 +14,5 @@ export {
ACTIVE_CLASS, ACTIVE_CLASS,
TEMPLATE_REGEX, TEMPLATE_REGEX,
IGNORE_CLASS, IGNORE_CLASS,
IGNORE_HIDING_CLASS,
}; };
import utils from './utils'; import utils from './utils';
import { SELECTED_CLASS, IGNORE_CLASS } from './constants'; import { SELECTED_CLASS, IGNORE_CLASS, IGNORE_HIDING_CLASS } from './constants';
class DropDown { class DropDown {
constructor(list) { constructor(list, config = {}) {
this.currentIndex = 0; this.currentIndex = 0;
this.hidden = true; this.hidden = true;
this.list = typeof list === 'string' ? document.querySelector(list) : list; this.list = typeof list === 'string' ? document.querySelector(list) : list;
this.items = []; this.items = [];
this.eventWrapper = {}; this.eventWrapper = {};
if (config.addActiveClassToDropdownButton) {
this.dropdownToggle = this.list.parentNode.querySelector('.js-dropdown-toggle');
}
this.getItems(); this.getItems();
this.initTemplateString(); this.initTemplateString();
this.addEvents(); this.addEvents();
...@@ -42,7 +45,7 @@ class DropDown { ...@@ -42,7 +45,7 @@ class DropDown {
this.addSelectedClass(selected); this.addSelectedClass(selected);
e.preventDefault(); e.preventDefault();
this.hide(); if (!e.target.classList.contains(IGNORE_HIDING_CLASS)) this.hide();
const listEvent = new CustomEvent('click.dl', { const listEvent = new CustomEvent('click.dl', {
detail: { detail: {
...@@ -67,7 +70,20 @@ class DropDown { ...@@ -67,7 +70,20 @@ class DropDown {
addEvents() { addEvents() {
this.eventWrapper.clickEvent = this.clickEvent.bind(this); this.eventWrapper.clickEvent = this.clickEvent.bind(this);
this.eventWrapper.closeDropdown = this.closeDropdown.bind(this);
this.list.addEventListener('click', this.eventWrapper.clickEvent); 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) { setData(data) {
...@@ -110,6 +126,8 @@ class DropDown { ...@@ -110,6 +126,8 @@ class DropDown {
this.list.style.display = 'block'; this.list.style.display = 'block';
this.currentIndex = 0; this.currentIndex = 0;
this.hidden = false; this.hidden = false;
if (this.dropdownToggle) this.dropdownToggle.classList.add('active');
} }
hide() { hide() {
...@@ -117,6 +135,8 @@ class DropDown { ...@@ -117,6 +135,8 @@ class DropDown {
this.list.style.display = 'none'; this.list.style.display = 'none';
this.currentIndex = 0; this.currentIndex = 0;
this.hidden = true; this.hidden = true;
if (this.dropdownToggle) this.dropdownToggle.classList.remove('active');
} }
toggle() { toggle() {
...@@ -128,6 +148,7 @@ class DropDown { ...@@ -128,6 +148,7 @@ class DropDown {
destroy() { destroy() {
this.hide(); this.hide();
this.list.removeEventListener('click', this.eventWrapper.clickEvent); this.list.removeEventListener('click', this.eventWrapper.clickEvent);
this.list.removeEventListener('keyup', this.eventWrapper.closeDropdown);
} }
static setImagesSrc(template) { static setImagesSrc(template) {
......
...@@ -3,7 +3,7 @@ import DropDown from './drop_down'; ...@@ -3,7 +3,7 @@ import DropDown from './drop_down';
class Hook { class Hook {
constructor(trigger, list, plugins, config) { constructor(trigger, list, plugins, config) {
this.trigger = trigger; this.trigger = trigger;
this.list = new DropDown(list); this.list = new DropDown(list, config);
this.type = 'Hook'; this.type = 'Hook';
this.event = 'click'; this.event = 'click';
this.plugins = plugins || []; this.plugins = plugins || [];
......
...@@ -41,7 +41,7 @@ const createFlashEl = (message, type, isInContentWrapper = false) => ` ...@@ -41,7 +41,7 @@ const createFlashEl = (message, type, isInContentWrapper = false) => `
`; `;
const removeFlashClickListener = (flashEl, fadeTransition) => { const removeFlashClickListener = (flashEl, fadeTransition) => {
flashEl.parentNode.addEventListener('click', () => hideFlash(flashEl, fadeTransition)); flashEl.addEventListener('click', () => hideFlash(flashEl, fadeTransition));
}; };
/* /*
......
...@@ -79,7 +79,7 @@ ...@@ -79,7 +79,7 @@
v-tooltip v-tooltip
v-if="showInlineEditButton && canUpdate" v-if="showInlineEditButton && canUpdate"
type="button" type="button"
class="btn-blank btn-edit note-action-button" class="btn btn-default btn-edit btn-svg"
v-html="pencilIcon" v-html="pencilIcon"
title="Edit title and description" title="Edit title and description"
data-placement="bottom" data-placement="bottom"
......
...@@ -190,7 +190,7 @@ export const insertText = (target, text) => { ...@@ -190,7 +190,7 @@ export const insertText = (target, text) => {
target.selectionStart = target.selectionEnd = selectionStart + insertedText.length; target.selectionStart = target.selectionEnd = selectionStart + insertedText.length;
// Trigger autosave // Trigger autosave
$(target).trigger('input'); target.dispatchEvent(new Event('input'));
// Trigger autosize // Trigger autosize
const event = document.createEvent('Event'); const event = document.createEvent('Event');
......
...@@ -44,7 +44,6 @@ import './commits'; ...@@ -44,7 +44,6 @@ import './commits';
import './compare'; import './compare';
import './compare_autocomplete'; import './compare_autocomplete';
import './confirm_danger_modal'; import './confirm_danger_modal';
import './copy_to_clipboard';
import './diff'; import './diff';
import './files_comment_button'; import './files_comment_button';
import Flash, { removeFlashClickListener } from './flash'; import Flash, { removeFlashClickListener } from './flash';
...@@ -319,6 +318,8 @@ $(function () { ...@@ -319,6 +318,8 @@ $(function () {
const flashContainer = document.querySelector('.flash-container'); const flashContainer = document.querySelector('.flash-container');
if (flashContainer && flashContainer.children.length) { if (flashContainer && flashContainer.children.length) {
removeFlashClickListener(flashContainer.children[0]); flashContainer.querySelectorAll('.flash-alert, .flash-notice, .flash-success').forEach((flashEl) => {
removeFlashClickListener(flashEl);
});
} }
}); });
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
noteType: constants.COMMENT, noteType: constants.COMMENT,
// Can't use mapGetters, // Can't use mapGetters,
// this needs to be in the data object because it belongs to the state // this needs to be in the data object because it belongs to the state
issueState: this.$store.getters.getIssueData.state, issueState: this.$store.getters.getNoteableData.state,
isSubmitting: false, isSubmitting: false,
isSubmitButtonDisabled: true, isSubmitButtonDisabled: true,
}; };
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
...mapGetters([ ...mapGetters([
'getCurrentUserLastNote', 'getCurrentUserLastNote',
'getUserData', 'getUserData',
'getIssueData', 'getNoteableData',
'getNotesData', 'getNotesData',
]), ]),
isLoggedIn() { isLoggedIn() {
...@@ -59,7 +59,7 @@ ...@@ -59,7 +59,7 @@
return this.issueState === constants.OPENED || this.issueState === constants.REOPENED; return this.issueState === constants.OPENED || this.issueState === constants.REOPENED;
}, },
canCreateNote() { canCreateNote() {
return this.getIssueData.current_user.can_create_note; return this.getNoteableData.current_user.can_create_note;
}, },
issueActionButtonTitle() { issueActionButtonTitle() {
if (this.note.length) { if (this.note.length) {
...@@ -85,16 +85,16 @@ ...@@ -85,16 +85,16 @@
return this.getNotesData.quickActionsDocsPath; return this.getNotesData.quickActionsDocsPath;
}, },
markdownPreviewPath() { markdownPreviewPath() {
return this.getIssueData.preview_note_path; return this.getNoteableData.preview_note_path;
}, },
author() { author() {
return this.getUserData; return this.getUserData;
}, },
canUpdateIssue() { canUpdateIssue() {
return this.getIssueData.current_user.can_update; return this.getNoteableData.current_user.can_update;
}, },
endpoint() { endpoint() {
return this.getIssueData.create_note_path; return this.getNoteableData.create_note_path;
}, },
}, },
methods: { methods: {
...@@ -119,7 +119,7 @@ ...@@ -119,7 +119,7 @@
data: { data: {
note: { note: {
noteable_type: constants.NOTEABLE_TYPE, noteable_type: constants.NOTEABLE_TYPE,
noteable_id: this.getIssueData.id, noteable_id: this.getNoteableData.id,
note: this.note, note: this.note,
}, },
}, },
...@@ -207,7 +207,7 @@ ...@@ -207,7 +207,7 @@
}, },
initAutoSave() { initAutoSave() {
if (this.isLoggedIn) { if (this.isLoggedIn) {
this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getIssueData.id], 'issue'); this.autosave = new Autosave($(this.$refs.textarea), ['Note', 'Issue', this.getNoteableData.id], 'issue');
} }
}, },
initTaskList() { initTaskList() {
...@@ -269,9 +269,9 @@ ...@@ -269,9 +269,9 @@
<div class="error-alert"></div> <div class="error-alert"></div>
<issue-warning <issue-warning
v-if="hasWarning(getIssueData)" v-if="hasWarning(getNoteableData)"
:is-locked="isLocked(getIssueData)" :is-locked="isLocked(getNoteableData)"
:is-confidential="isConfidential(getIssueData)" :is-confidential="isConfidential(getNoteableData)"
/> />
<markdown-field <markdown-field
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
], ],
computed: { computed: {
...mapGetters([ ...mapGetters([
'getIssueData', 'getNoteableData',
]), ]),
discussion() { discussion() {
return this.note.notes[0]; return this.note.notes[0];
...@@ -48,10 +48,10 @@ ...@@ -48,10 +48,10 @@
return this.discussion.author; return this.discussion.author;
}, },
canReply() { canReply() {
return this.getIssueData.current_user.can_create_note; return this.getNoteableData.current_user.can_create_note;
}, },
newNotePath() { newNotePath() {
return this.getIssueData.create_note_path; return this.getNoteableData.create_note_path;
}, },
lastUpdatedBy() { lastUpdatedBy() {
const { notes } = this.note; const { notes } = this.note;
......
...@@ -46,8 +46,8 @@ ...@@ -46,8 +46,8 @@
computed: { computed: {
...mapGetters([ ...mapGetters([
'getDiscussionLastNote', 'getDiscussionLastNote',
'getIssueData', 'getNoteableData',
'getIssueDataByProp', 'getNoteableDataByProp',
'getNotesDataByProp', 'getNotesDataByProp',
'getUserDataByProp', 'getUserDataByProp',
]), ]),
...@@ -55,7 +55,7 @@ ...@@ -55,7 +55,7 @@
return `#note_${this.noteId}`; return `#note_${this.noteId}`;
}, },
markdownPreviewPath() { markdownPreviewPath() {
return this.getIssueDataByProp('preview_note_path'); return this.getNoteableDataByProp('preview_note_path');
}, },
markdownDocsPath() { markdownDocsPath() {
return this.getNotesDataByProp('markdownDocsPath'); return this.getNotesDataByProp('markdownDocsPath');
...@@ -129,9 +129,9 @@ ...@@ -129,9 +129,9 @@
class="edit-note common-note-form js-quick-submit gfm-form"> class="edit-note common-note-form js-quick-submit gfm-form">
<issue-warning <issue-warning
v-if="hasWarning(getIssueData)" v-if="hasWarning(getNoteableData)"
:is-locked="isLocked(getIssueData)" :is-locked="isLocked(getNoteableData)"
:is-confidential="isConfidential(getIssueData)" :is-confidential="isConfidential(getNoteableData)"
/> />
<markdown-field <markdown-field
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
export default { export default {
name: 'issueNotesApp', name: 'issueNotesApp',
props: { props: {
issueData: { noteableData: {
type: Object, type: Object,
required: true, required: true,
}, },
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
actionToggleAward: 'toggleAward', actionToggleAward: 'toggleAward',
scrollToNoteIfNeeded: 'scrollToNoteIfNeeded', scrollToNoteIfNeeded: 'scrollToNoteIfNeeded',
setNotesData: 'setNotesData', setNotesData: 'setNotesData',
setIssueData: 'setIssueData', setNoteableData: 'setNoteableData',
setUserData: 'setUserData', setUserData: 'setUserData',
setLastFetchedAt: 'setLastFetchedAt', setLastFetchedAt: 'setLastFetchedAt',
setTargetNoteHash: 'setTargetNoteHash', setTargetNoteHash: 'setTargetNoteHash',
...@@ -106,7 +106,7 @@ ...@@ -106,7 +106,7 @@
}, },
created() { created() {
this.setNotesData(this.notesData); this.setNotesData(this.notesData);
this.setIssueData(this.issueData); this.setNoteableData(this.noteableData);
this.setUserData(this.userData); this.setUserData(this.userData);
}, },
mounted() { mounted() {
......
...@@ -10,7 +10,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -10,7 +10,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
const notesDataset = document.getElementById('js-vue-notes').dataset; const notesDataset = document.getElementById('js-vue-notes').dataset;
return { return {
issueData: JSON.parse(notesDataset.issueData), noteableData: JSON.parse(notesDataset.noteableData),
currentUserData: JSON.parse(notesDataset.currentUserData), currentUserData: JSON.parse(notesDataset.currentUserData),
notesData: { notesData: {
lastFetchedAt: notesDataset.lastFetchedAt, lastFetchedAt: notesDataset.lastFetchedAt,
...@@ -26,7 +26,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -26,7 +26,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
render(createElement) { render(createElement) {
return createElement('issue-notes-app', { return createElement('issue-notes-app', {
props: { props: {
issueData: this.issueData, noteableData: this.noteableData,
notesData: this.notesData, notesData: this.notesData,
userData: this.currentUserData, userData: this.currentUserData,
}, },
......
...@@ -12,7 +12,7 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils'; ...@@ -12,7 +12,7 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
let eTagPoll; let eTagPoll;
export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data); export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data);
export const setIssueData = ({ commit }, data) => commit(types.SET_ISSUE_DATA, data); export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data);
export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data); export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data);
export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data); export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);
export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data); export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data);
......
...@@ -6,8 +6,8 @@ export const targetNoteHash = state => state.targetNoteHash; ...@@ -6,8 +6,8 @@ export const targetNoteHash = state => state.targetNoteHash;
export const getNotesData = state => state.notesData; export const getNotesData = state => state.notesData;
export const getNotesDataByProp = state => prop => state.notesData[prop]; export const getNotesDataByProp = state => prop => state.notesData[prop];
export const getIssueData = state => state.issueData; export const getNoteableData = state => state.noteableData;
export const getIssueDataByProp = state => prop => state.issueData[prop]; export const getNoteableDataByProp = state => prop => state.noteableData[prop];
export const getUserData = state => state.userData || {}; export const getUserData = state => state.userData || {};
export const getUserDataByProp = state => prop => state.userData && state.userData[prop]; export const getUserDataByProp = state => prop => state.userData && state.userData[prop];
......
...@@ -15,7 +15,7 @@ export default new Vuex.Store({ ...@@ -15,7 +15,7 @@ export default new Vuex.Store({
// holds endpoints and permissions provided through haml // holds endpoints and permissions provided through haml
notesData: {}, notesData: {},
userData: {}, userData: {},
issueData: {}, noteableData: {},
}, },
actions, actions,
getters, getters,
......
...@@ -3,7 +3,7 @@ export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION'; ...@@ -3,7 +3,7 @@ export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION';
export const DELETE_NOTE = 'DELETE_NOTE'; export const DELETE_NOTE = 'DELETE_NOTE';
export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES'; export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES';
export const SET_NOTES_DATA = 'SET_NOTES_DATA'; export const SET_NOTES_DATA = 'SET_NOTES_DATA';
export const SET_ISSUE_DATA = 'SET_ISSUE_DATA'; export const SET_NOTEABLE_DATA = 'SET_NOTEABLE_DATA';
export const SET_USER_DATA = 'SET_USER_DATA'; export const SET_USER_DATA = 'SET_USER_DATA';
export const SET_INITIAL_NOTES = 'SET_INITIAL_NOTES'; export const SET_INITIAL_NOTES = 'SET_INITIAL_NOTES';
export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT'; export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT';
......
...@@ -66,8 +66,8 @@ export default { ...@@ -66,8 +66,8 @@ export default {
Object.assign(state, { notesData: data }); Object.assign(state, { notesData: data });
}, },
[types.SET_ISSUE_DATA](state, data) { [types.SET_NOTEABLE_DATA](state, data) {
Object.assign(state, { issueData: data }); Object.assign(state, { noteableData: data });
}, },
[types.SET_USER_DATA](state, data) { [types.SET_USER_DATA](state, data) {
......
...@@ -17,13 +17,14 @@ export default class Project { ...@@ -17,13 +17,14 @@ export default class Project {
$('a', $cloneOptions).on('click', (e) => { $('a', $cloneOptions).on('click', (e) => {
const $this = $(e.currentTarget); const $this = $(e.currentTarget);
const url = $this.attr('href'); const url = $this.attr('href');
const activeText = $this.find('.dropdown-menu-inner-title').text();
e.preventDefault(); e.preventDefault();
$('.is-active', $cloneOptions).not($this).removeClass('is-active'); $('.is-active', $cloneOptions).not($this).removeClass('is-active');
$this.toggleClass('is-active'); $this.toggleClass('is-active');
$projectCloneField.val(url); $projectCloneField.val(url);
$cloneBtnText.text($this.text()); $cloneBtnText.text(activeText);
$('#modal-geo-info').data({ $('#modal-geo-info').data({
cloneUrlSecondary: $this.attr('href'), cloneUrlSecondary: $this.attr('href'),
......
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 { ...@@ -48,6 +48,27 @@ export default {
} }
return this.projectName; 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> </script>
...@@ -87,9 +108,7 @@ export default { ...@@ -87,9 +108,7 @@ export default {
<div <div
class="project-namespace" class="project-namespace"
:title="namespace" :title="namespace"
> >{{truncatedNamespace}}</div>
{{namespace}}
</div>
</div> </div>
</a> </a>
</li> </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 { ...@@ -40,20 +40,24 @@ export default {
</script> </script>
<template> <template>
<div class="repository-view"> <div
<div class="tree-content-holder" :class="{'tree-content-holder-mini' : isCollapsed}"> class="multi-file"
<repo-sidebar/> :class="{
<div 'is-collapsed': isCollapsed
v-if="isCollapsed" }"
class="panel-right" >
> <repo-sidebar/>
<repo-tabs/> <div
<component v-if="isCollapsed"
:is="currentBlobView" class="multi-file-edit-pane"
/> >
<repo-file-buttons/> <repo-tabs />
</div> <component
class="multi-file-edit-pane-content"
:is="currentBlobView"
/>
<repo-file-buttons />
</div> </div>
<repo-commit-section v-if="changedFiles.length" /> <repo-commit-section />
</div> </div>
</template> </template>
<script> <script>
import { mapGetters, mapState, mapActions } from 'vuex'; 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 PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import { n__ } from '../../locale'; import commitFilesList from './commit_sidebar/list.vue';
export default { export default {
components: { components: {
PopupDialog, PopupDialog,
icon,
commitFilesList,
},
directives: {
tooltip,
}, },
data() { data() {
return { return {
...@@ -13,6 +20,7 @@ export default { ...@@ -13,6 +20,7 @@ export default {
submitCommitsLoading: false, submitCommitsLoading: false,
startNewMR: false, startNewMR: false,
commitMessage: '', commitMessage: '',
collapsed: true,
}; };
}, },
computed: { computed: {
...@@ -23,10 +31,10 @@ export default { ...@@ -23,10 +31,10 @@ export default {
'changedFiles', 'changedFiles',
]), ]),
commitButtonDisabled() { commitButtonDisabled() {
return !this.commitMessage || this.submitCommitsLoading; return this.commitMessage === '' || this.submitCommitsLoading || !this.changedFiles.length;
}, },
commitButtonText() { commitMessageCount() {
return n__('Commit %d file', 'Commit %d files', this.changedFiles.length); return this.commitMessage.length;
}, },
}, },
methods: { methods: {
...@@ -77,12 +85,20 @@ export default { ...@@ -77,12 +85,20 @@ export default {
this.submitCommitsLoading = false; this.submitCommitsLoading = false;
}); });
}, },
toggleCollapsed() {
this.collapsed = !this.collapsed;
},
}, },
}; };
</script> </script>
<template> <template>
<div id="commit-area"> <div
class="multi-file-commit-panel"
:class="{
'is-collapsed': collapsed,
}"
>
<popup-dialog <popup-dialog
v-if="showNewBranchDialog" v-if="showNewBranchDialog"
:primary-button-label="__('Create new branch')" :primary-button-label="__('Create new branch')"
...@@ -92,78 +108,71 @@ export default { ...@@ -92,78 +108,71 @@ export default {
@toggle="showNewBranchDialog = false" @toggle="showNewBranchDialog = false"
@submit="makeCommit(true)" @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 <form
class="form-horizontal" class="form-horizontal multi-file-commit-form"
@submit.prevent="tryCommit()"> @submit.prevent="tryCommit"
<fieldset> v-if="!collapsed"
<div class="form-group"> >
<label class="col-md-4 control-label staged-files"> <div class="multi-file-commit-fieldset">
Staged files ({{changedFiles.length}}) <textarea
</label> class="form-control multi-file-commit-message"
<div class="col-md-6"> name="commit-message"
<ul class="list-unstyled changed-files"> v-model="commitMessage"
<li placeholder="Commit message"
v-for="(file, index) in changedFiles" >
:key="index"> </textarea>
<span class="help-block"> </div>
{{ file.path }} <div class="multi-file-commit-fieldset">
</span> <label
</li> v-tooltip
</ul> title="Create a new merge request with these changes"
</div> data-container="body"
</div> data-placement="top"
<div class="form-group"> >
<label <input
class="col-md-4 control-label" type="checkbox"
for="commit-message"> v-model="startNewMR"
Commit message />
</label> Merge Request
<div class="col-md-6"> </label>
<textarea <button
id="commit-message" type="submit"
class="form-control" :disabled="commitButtonDisabled"
name="commit-message" class="btn btn-default btn-sm append-right-10 prepend-left-10"
v-model="commitMessage"> >
</textarea> <i
</div> v-if="submitCommitsLoading"
</div> class="js-commit-loading-icon fa fa-spinner fa-spin"
<div class="form-group target-branch"> aria-hidden="true"
<label aria-label="loading"
class="col-md-4 control-label" >
for="target-branch"> </i>
Target branch Commit
</label> </button>
<div class="col-md-6"> <div
<span class="help-block"> class="multi-file-commit-message-count"
{{currentBranch}} >
</span> {{ commitMessageCount }}
</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> </div>
</fieldset> </div>
</form> </form>
</div> </div>
</template> </template>
...@@ -3,19 +3,18 @@ ...@@ -3,19 +3,18 @@
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import flash from '../../flash'; import flash from '../../flash';
import monacoLoader from '../monaco_loader'; import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
export default { export default {
destroyed() { beforeDestroy() {
if (this.monacoInstance) { this.editor.dispose();
this.monacoInstance.destroy();
}
}, },
mounted() { mounted() {
if (this.monaco) { if (this.editor && monaco) {
this.initMonaco(); this.initMonaco();
} else { } else {
monacoLoader(['vs/editor/editor.main'], () => { monacoLoader(['vs/editor/editor.main'], () => {
this.monaco = monaco; this.editor = Editor.create(monaco);
this.initMonaco(); this.initMonaco();
}); });
...@@ -29,50 +28,25 @@ export default { ...@@ -29,50 +28,25 @@ export default {
initMonaco() { initMonaco() {
if (this.shouldHideEditor) return; if (this.shouldHideEditor) return;
if (this.monacoInstance) { this.editor.clearEditor();
this.monacoInstance.setModel(null);
}
this.getRawFileData(this.activeFile) this.getRawFileData(this.activeFile)
.then(() => { .then(() => {
if (!this.monacoInstance) { this.editor.createInstance(this.$refs.editor);
this.monacoInstance = this.monaco.editor.create(this.$el, {
model: null,
readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
});
this.languages = this.monaco.languages.getLanguages();
this.addMonacoEvents();
}
this.setupEditor();
}) })
.then(() => this.setupEditor())
.catch(() => flash('Error setting up monaco. Please try again.')); .catch(() => flash('Error setting up monaco. Please try again.'));
}, },
setupEditor() { setupEditor() {
if (!this.activeFile) return; if (!this.activeFile) return;
const content = this.activeFile.content !== '' ? this.activeFile.content : this.activeFile.raw;
const foundLang = this.languages.find(lang => const model = this.editor.createModel(this.activeFile);
lang.extensions && lang.extensions.indexOf(this.activeFileExtension) === 0,
);
const newModel = this.monaco.editor.createModel(
content, foundLang ? foundLang.id : 'plaintext',
);
this.monacoInstance.setModel(newModel); this.editor.attachModel(model);
this.monacoInstance.updateOptions({ model.onChange((m) => {
readOnly: !!this.activeFile.file_lock,
});
},
addMonacoEvents() {
this.monacoInstance.onKeyUp(() => {
this.changeFileContent({ this.changeFileContent({
file: this.activeFile, file: this.activeFile,
content: this.monacoInstance.getValue(), content: m.getValue(),
}); });
}); });
}, },
...@@ -102,9 +76,14 @@ export default { ...@@ -102,9 +76,14 @@ export default {
class="blob-viewer-container blob-editor-container" class="blob-viewer-container blob-editor-container"
> >
<div <div
v-if="shouldHideEditor" v-show="shouldHideEditor"
v-html="activeFile.html" v-html="activeFile.html"
> >
</div> </div>
<div
v-show="!shouldHideEditor"
ref="editor"
>
</div>
</div> </div>
</template> </template>
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
class="file" class="file"
@click.prevent="clickedTreeRow(file)"> @click.prevent="clickedTreeRow(file)">
<td <td
class="multi-file-table-col-name" class="multi-file-table-name"
:colspan="submoduleColSpan" :colspan="submoduleColSpan"
> >
<i <i
...@@ -90,12 +90,11 @@ ...@@ -90,12 +90,11 @@
</td> </td>
<template v-if="!isCollapsed && !isSubmodule"> <template v-if="!isCollapsed && !isSubmodule">
<td class="hidden-sm hidden-xs"> <td class="multi-file-table-col-commit-message hidden-sm hidden-xs">
<a <a
v-if="file.lastCommit.message" v-if="file.lastCommit.message"
@click.stop @click.stop
:href="file.lastCommit.url" :href="file.lastCommit.url"
class="commit-message"
> >
{{ file.lastCommit.message }} {{ file.lastCommit.message }}
</a> </a>
......
...@@ -22,12 +22,12 @@ export default { ...@@ -22,12 +22,12 @@ export default {
<template> <template>
<div <div
v-if="showButtons" v-if="showButtons"
class="repo-file-buttons" class="multi-file-editor-btn-group"
> >
<a <a
:href="activeFile.rawPath" :href="activeFile.rawPath"
target="_blank" target="_blank"
class="btn btn-default raw" class="btn btn-default btn-sm raw"
rel="noopener noreferrer"> rel="noopener noreferrer">
{{ rawDownloadButtonLabel }} {{ rawDownloadButtonLabel }}
</a> </a>
...@@ -38,17 +38,17 @@ export default { ...@@ -38,17 +38,17 @@ export default {
aria-label="File actions"> aria-label="File actions">
<a <a
:href="activeFile.blamePath" :href="activeFile.blamePath"
class="btn btn-default blame"> class="btn btn-default btn-sm blame">
Blame Blame
</a> </a>
<a <a
:href="activeFile.commitsPath" :href="activeFile.commitsPath"
class="btn btn-default history"> class="btn btn-default btn-sm history">
History History
</a> </a>
<a <a
:href="activeFile.permalink" :href="activeFile.permalink"
class="btn btn-default permalink"> class="btn btn-default btn-sm permalink">
Permalink Permalink
</a> </a>
</div> </div>
......
...@@ -32,10 +32,12 @@ export default { ...@@ -32,10 +32,12 @@ export default {
</script> </script>
<template> <template>
<div class="blob-viewer-container"> <div>
<div <div
v-if="!activeFile.renderError" v-if="!activeFile.renderError"
v-html="activeFile.html"> v-html="activeFile.html"
class="multi-file-preview-holder"
>
</div> </div>
<div <div
v-else-if="activeFile.tempFile" v-else-if="activeFile.tempFile"
......
...@@ -44,20 +44,16 @@ export default { ...@@ -44,20 +44,16 @@ export default {
</script> </script>
<template> <template>
<div id="sidebar" :class="{'sidebar-mini' : isCollapsed}"> <div class="ide-file-list">
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th <th
v-if="isCollapsed" v-if="isCollapsed"
class="repo-file-options title"
> >
<strong class="clgray">
{{ projectName }}
</strong>
</th> </th>
<template v-else> <template v-else>
<th class="name multi-file-table-col-name"> <th class="name multi-file-table-name">
Name Name
</th> </th>
<th class="hidden-sm hidden-xs last-commit"> <th class="hidden-sm hidden-xs last-commit">
...@@ -79,7 +75,7 @@ export default { ...@@ -79,7 +75,7 @@ export default {
:key="n" :key="n"
/> />
<repo-file <repo-file
v-for="(file, index) in treeList" v-for="file in treeList"
:key="file.key" :key="file.key"
:file="file" :file="file"
/> />
......
...@@ -41,30 +41,35 @@ export default { ...@@ -41,30 +41,35 @@ export default {
<template> <template>
<li <li
:class="{ active : tab.active }"
@click="setFileActive(tab)" @click="setFileActive(tab)"
> >
<button <button
type="button" type="button"
class="close-btn" class="multi-file-tab-close"
@click.stop.prevent="closeFile({ file: tab })" @click.stop.prevent="closeFile({ file: tab })"
:aria-label="closeLabel"> :aria-label="closeLabel"
:class="{
'modified': tab.changed,
}"
:disabled="tab.changed"
>
<i <i
class="fa" class="fa"
:class="changedClass" :class="changedClass"
aria-hidden="true"> aria-hidden="true"
>
</i> </i>
</button> </button>
<a <div
href="#" class="multi-file-tab"
class="repo-tab" :class="{active : tab.active }"
:title="tab.url" :title="tab.url"
@click.prevent.stop="setFileActive(tab)"> >
{{tab.name}} {{ tab.name }}
<fileStatusIcon <file-status-icon
:file="tab"> :file="tab"
</fileStatusIcon> />
</a> </div>
</li> </li>
</template> </template>
...@@ -16,14 +16,12 @@ ...@@ -16,14 +16,12 @@
<template> <template>
<ul <ul
id="tabs" class="multi-file-tabs list-unstyled append-bottom-0"
class="list-unstyled"
> >
<repo-tab <repo-tab
v-for="tab in openFiles" v-for="tab in openFiles"
:key="tab.id" :key="tab.id"
:tab="tab" :tab="tab"
/> />
<li class="tabs-divider" />
</ul> </ul>
</template> </template>
export default class Disposable {
constructor() {
this.disposers = new Set();
}
add(...disposers) {
disposers.forEach(disposer => this.disposers.add(disposer));
}
dispose() {
this.disposers.forEach(disposer => disposer.dispose());
this.disposers.clear();
}
}
/* global monaco */
import Disposable from './disposable';
export default class Model {
constructor(monaco, file) {
this.monaco = monaco;
this.disposable = new Disposable();
this.file = file;
this.content = file.content !== '' ? file.content : file.raw;
this.disposable.add(
this.originalModel = this.monaco.editor.createModel(
this.file.raw,
undefined,
new this.monaco.Uri(null, null, `original/${this.file.path}`),
),
this.model = this.monaco.editor.createModel(
this.content,
undefined,
new this.monaco.Uri(null, null, this.file.path),
),
);
this.events = new Map();
}
get url() {
return this.model.uri.toString();
}
get path() {
return this.file.path;
}
getModel() {
return this.model;
}
getOriginalModel() {
return this.originalModel;
}
onChange(cb) {
this.events.set(
this.path,
this.disposable.add(
this.model.onDidChangeContent(e => cb(this.model, e)),
),
);
}
dispose() {
this.disposable.dispose();
this.events.clear();
}
}
import Disposable from './disposable';
import Model from './model';
export default class ModelManager {
constructor(monaco) {
this.monaco = monaco;
this.disposable = new Disposable();
this.models = new Map();
}
hasCachedModel(path) {
return this.models.has(path);
}
addModel(file) {
if (this.hasCachedModel(file.path)) {
return this.models.get(file.path);
}
const model = new Model(this.monaco, file);
this.models.set(model.path, model);
this.disposable.add(model);
return model;
}
dispose() {
// dispose of all the models
this.disposable.dispose();
this.models.clear();
}
}
export default class DecorationsController {
constructor(editor) {
this.editor = editor;
this.decorations = new Map();
this.editorDecorations = new Map();
}
getAllDecorationsForModel(model) {
if (!this.decorations.has(model.url)) return [];
const modelDecorations = this.decorations.get(model.url);
const decorations = [];
modelDecorations.forEach(val => decorations.push(...val));
return decorations;
}
addDecorations(model, decorationsKey, decorations) {
const decorationMap = this.decorations.get(model.url) || new Map();
decorationMap.set(decorationsKey, decorations);
this.decorations.set(model.url, decorationMap);
this.decorate(model);
}
decorate(model) {
const decorations = this.getAllDecorationsForModel(model);
const oldDecorations = this.editorDecorations.get(model.url) || [];
this.editorDecorations.set(
model.url,
this.editor.instance.deltaDecorations(oldDecorations, decorations),
);
}
dispose() {
this.decorations.clear();
this.editorDecorations.clear();
}
}
/* global monaco */
import { throttle } from 'underscore';
import DirtyDiffWorker from './diff_worker';
import Disposable from '../common/disposable';
export const getDiffChangeType = (change) => {
if (change.modified) {
return 'modified';
} else if (change.added) {
return 'added';
} else if (change.removed) {
return 'removed';
}
return '';
};
export const getDecorator = change => ({
range: new monaco.Range(
change.lineNumber,
1,
change.endLineNumber,
1,
),
options: {
isWholeLine: true,
linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
},
});
export default class DirtyDiffController {
constructor(modelManager, decorationsController) {
this.disposable = new Disposable();
this.editorSimpleWorker = null;
this.modelManager = modelManager;
this.decorationsController = decorationsController;
this.dirtyDiffWorker = new DirtyDiffWorker();
this.throttledComputeDiff = throttle(this.computeDiff, 250);
this.decorate = this.decorate.bind(this);
this.dirtyDiffWorker.addEventListener('message', this.decorate);
}
attachModel(model) {
model.onChange(() => this.throttledComputeDiff(model));
}
computeDiff(model) {
this.dirtyDiffWorker.postMessage({
path: model.path,
originalContent: model.getOriginalModel().getValue(),
newContent: model.getModel().getValue(),
});
}
reDecorate(model) {
this.decorationsController.decorate(model);
}
decorate({ data }) {
const decorations = data.changes.map(change => getDecorator(change));
this.decorationsController.addDecorations(data.path, 'dirtyDiff', decorations);
}
dispose() {
this.disposable.dispose();
this.dirtyDiffWorker.removeEventListener('message', this.decorate);
this.dirtyDiffWorker.terminate();
}
}
import { diffLines } from 'diff';
// eslint-disable-next-line import/prefer-default-export
export const computeDiff = (originalContent, newContent) => {
const changes = diffLines(originalContent, newContent);
let lineNumber = 1;
return changes.reduce((acc, change) => {
const findOnLine = acc.find(c => c.lineNumber === lineNumber);
if (findOnLine) {
Object.assign(findOnLine, change, {
modified: true,
endLineNumber: (lineNumber + change.count) - 1,
});
} else if ('added' in change || 'removed' in change) {
acc.push(Object.assign({}, change, {
lineNumber,
modified: undefined,
endLineNumber: (lineNumber + change.count) - 1,
}));
}
if (!change.removed) {
lineNumber += change.count;
}
return acc;
}, []);
};
import { computeDiff } from './diff';
self.addEventListener('message', (e) => {
const data = e.data;
self.postMessage({
path: data.path,
changes: computeDiff(data.originalContent, data.newContent),
});
});
import DecorationsController from './decorations/controller';
import DirtyDiffController from './diff/controller';
import Disposable from './common/disposable';
import ModelManager from './common/model_manager';
import editorOptions from './editor_options';
export default class Editor {
static create(monaco) {
this.editorInstance = new Editor(monaco);
return this.editorInstance;
}
constructor(monaco) {
this.monaco = monaco;
this.currentModel = null;
this.instance = null;
this.dirtyDiffController = null;
this.disposable = new Disposable();
this.disposable.add(
this.modelManager = new ModelManager(this.monaco),
this.decorationsController = new DecorationsController(this),
);
}
createInstance(domElement) {
if (!this.instance) {
this.disposable.add(
this.instance = this.monaco.editor.create(domElement, {
model: null,
readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
}),
this.dirtyDiffController = new DirtyDiffController(
this.modelManager, this.decorationsController,
),
);
}
}
createModel(file) {
return this.modelManager.addModel(file);
}
attachModel(model) {
this.instance.setModel(model.getModel());
this.dirtyDiffController.attachModel(model);
this.currentModel = model;
this.instance.updateOptions(editorOptions.reduce((acc, obj) => {
Object.keys(obj).forEach((key) => {
Object.assign(acc, {
[key]: obj[key](model),
});
});
return acc;
}, {}));
this.dirtyDiffController.reDecorate(model);
}
clearEditor() {
if (this.instance) {
this.instance.setModel(null);
}
}
dispose() {
this.disposable.dispose();
// dispose main monaco instance
if (this.instance) {
this.instance = null;
}
}
}
export default [{
readOnly: model => !!model.file.file_lock,
}];
...@@ -16,6 +16,10 @@ export default { ...@@ -16,6 +16,10 @@ export default {
return Promise.resolve(file.content); return Promise.resolve(file.content);
} }
if (file.raw) {
return Promise.resolve(file.raw);
}
return Vue.http.get(file.rawPath, { params: { format: 'json' } }) return Vue.http.get(file.rawPath, { params: { format: 'json' } })
.then(res => res.text()); .then(res => res.text());
}, },
......
...@@ -34,3 +34,7 @@ export const canEditFile = (state) => { ...@@ -34,3 +34,7 @@ export const canEditFile = (state) => {
openedFiles.length && openedFiles.length &&
(currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary); (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);
...@@ -125,7 +125,7 @@ ...@@ -125,7 +125,7 @@
@include transition(border-color); @include transition(border-color);
} }
.note-action-button .link-highlight, .note-action-button,
.toolbar-btn, .toolbar-btn,
.dropdown-toggle-caret { .dropdown-toggle-caret {
@include transition(color); @include transition(color);
......
...@@ -88,17 +88,6 @@ ...@@ -88,17 +88,6 @@
border-color: $border-dark; border-color: $border-dark;
color: $color; color: $color;
} }
svg {
path {
fill: $color;
}
use {
stroke: $color;
}
}
} }
@mixin btn-green { @mixin btn-green {
...@@ -142,6 +131,13 @@ ...@@ -142,6 +131,13 @@
} }
} }
@mixin btn-svg {
height: $gl-padding;
width: $gl-padding;
top: 0;
vertical-align: text-top;
}
.btn { .btn {
@include btn-default; @include btn-default;
@include btn-white; @include btn-white;
...@@ -444,3 +440,7 @@ ...@@ -444,3 +440,7 @@
text-decoration: none; text-decoration: none;
} }
} }
.btn-svg svg {
@include btn-svg;
}
...@@ -2,14 +2,43 @@ ...@@ -2,14 +2,43 @@
.cgray { color: $common-gray; } .cgray { color: $common-gray; }
.clgray { color: $common-gray-light; } .clgray { color: $common-gray-light; }
.cred { color: $common-red; } .cred { color: $common-red; }
svg.cred { fill: $common-red; }
.cgreen { color: $common-green; } .cgreen { color: $common-green; }
svg.cgreen { fill: $common-green; }
.cdark { color: $common-gray-dark; } .cdark { color: $common-gray-dark; }
.text-plain,
.text-plain:hover {
color: $gl-text-color;
}
.text-secondary { .text-secondary {
color: $gl-text-color-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; } .underlined-link { text-decoration: underline; }
.hint { font-style: italic; color: $hint-color; } .hint { font-style: italic; color: $hint-color; }
.light { color: $common-gray; } .light { color: $common-gray; }
......
...@@ -1002,6 +1002,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu { ...@@ -1002,6 +1002,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
max-width: 250px; max-width: 250px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap;
} }
&:hover { &:hover {
......
...@@ -34,8 +34,15 @@ ...@@ -34,8 +34,15 @@
} }
} }
.flash-success {
@extend .alert;
@extend .alert-success;
margin: 0;
}
.flash-notice, .flash-notice,
.flash-alert { .flash-alert,
.flash-success {
border-radius: $border-radius-default; border-radius: $border-radius-default;
.container-fluid, .container-fluid,
...@@ -48,7 +55,8 @@ ...@@ -48,7 +55,8 @@
margin-bottom: 0; margin-bottom: 0;
.flash-notice, .flash-notice,
.flash-alert { .flash-alert,
.flash-success {
border-radius: 0; border-radius: 0;
} }
} }
......
.ci-status-icon-success, .ci-status-icon-success,
.ci-status-icon-passed { .ci-status-icon-passed {
color: $green-500; svg {
fill: $green-500;
}
} }
.ci-status-icon-failed { .ci-status-icon-failed {
color: $gl-danger; svg {
fill: $gl-danger;
}
} }
.ci-status-icon-pending, .ci-status-icon-pending,
.ci-status-icon-failed_with_warnings, .ci-status-icon-failed_with_warnings,
.ci-status-icon-success_with_warnings { .ci-status-icon-success_with_warnings {
color: $orange-500; svg {
fill: $orange-500;
}
} }
.ci-status-icon-running { .ci-status-icon-running {
color: $blue-400; svg {
fill: $blue-400;
}
} }
.ci-status-icon-canceled, .ci-status-icon-canceled,
.ci-status-icon-disabled, .ci-status-icon-disabled,
.ci-status-icon-not-found { .ci-status-icon-not-found {
color: $gl-text-color; svg {
fill: $gl-text-color;
}
} }
.ci-status-icon-created, .ci-status-icon-created,
.ci-status-icon-skipped { .ci-status-icon-skipped {
color: $gray-darkest; svg {
fill: $gray-darkest;
}
} }
.ci-status-icon-manual { .ci-status-icon-manual {
color: $gl-text-color; svg {
fill: $gl-text-color;
}
} }
.icon-link { .icon-link {
......
...@@ -195,33 +195,6 @@ summary { ...@@ -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 // Prevent datetimes on tooltips to break into two lines
.local-timeago { .local-timeago {
white-space: nowrap; white-space: nowrap;
......
...@@ -70,14 +70,13 @@ ...@@ -70,14 +70,13 @@
.title { .title {
padding: 0; padding: 0;
margin-bottom: 16px; margin-bottom: $gl-padding;
border-bottom: 0; border-bottom: 0;
} }
.btn-edit { .btn-edit {
margin-left: auto; margin-left: auto;
// Set height to match title height height: $gl-padding * 2;
height: 2em;
} }
// Border around images in issue and MR descriptions. // Border around images in issue and MR descriptions.
......
...@@ -218,7 +218,24 @@ ul.related-merge-requests > li { ...@@ -218,7 +218,24 @@ ul.related-merge-requests > li {
} }
.create-mr-dropdown-wrap { .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) { .btn-group:not(.hide) {
display: flex; display: flex;
...@@ -229,15 +246,16 @@ ul.related-merge-requests > li { ...@@ -229,15 +246,16 @@ ul.related-merge-requests > li {
flex-shrink: 0; flex-shrink: 0;
} }
.dropdown-menu { .create-merge-request-dropdown-menu {
width: 300px; width: 300px;
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
transform: translateY(0); transform: translateY(0);
display: none; display: none;
margin-top: 4px;
} }
.dropdown-toggle { .create-merge-request-dropdown-toggle {
.fa-caret-down { .fa-caret-down {
pointer-events: none; pointer-events: none;
color: inherit; color: inherit;
...@@ -245,18 +263,50 @@ ul.related-merge-requests > li { ...@@ -245,18 +263,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) { li:not(.divider) {
padding: 8px 9px;
&:last-child {
padding-bottom: 8px;
}
&.droplab-item-selected { &.droplab-item-selected {
.icon-container { .icon-container {
i { i {
visibility: visible; visibility: visible;
} }
} }
.description {
display: block;
}
}
&.droplab-item-ignore {
padding-top: 8px;
} }
.icon-container { .icon-container {
float: left; float: left;
padding-left: 6px;
i { i {
visibility: hidden; visibility: hidden;
...@@ -264,13 +314,12 @@ ul.related-merge-requests > li { ...@@ -264,13 +314,12 @@ ul.related-merge-requests > li {
} }
.description { .description {
padding-left: 30px; padding-left: 22px;
font-size: 13px; }
strong { input,
display: block; span {
font-weight: $gl-font-weight-bold; margin: 4px 0 0;
}
} }
} }
} }
......
...@@ -543,10 +543,7 @@ ul.notes { ...@@ -543,10 +543,7 @@ ul.notes {
} }
svg { svg {
height: 16px; @include btn-svg;
width: 16px;
top: 0;
vertical-align: text-top;
} }
.award-control-icon-positive, .award-control-icon-positive,
...@@ -780,12 +777,6 @@ ul.notes { ...@@ -780,12 +777,6 @@ ul.notes {
} }
} }
svg {
fill: currentColor;
height: 16px;
width: 16px;
}
.loading { .loading {
margin: 0; margin: 0;
height: auto; height: auto;
......
...@@ -299,14 +299,7 @@ ...@@ -299,14 +299,7 @@
} }
svg { svg {
fill: $layout-link-gray;
path {
fill: $layout-link-gray;
}
use {
stroke: $layout-link-gray;
}
} }
.fa-caret-down { .fa-caret-down {
...@@ -410,6 +403,18 @@ ...@@ -410,6 +403,18 @@
} }
} }
} }
.clone-dropdown-btn {
background-color: $white-light;
}
.clone-options-dropdown {
min-width: 240px;
.dropdown-menu-inner-content {
min-width: 320px;
}
}
} }
.project-repo-buttons { .project-repo-buttons {
...@@ -893,10 +898,6 @@ pre.light-well { ...@@ -893,10 +898,6 @@ pre.light-well {
font-size: $gl-font-size; font-size: $gl-font-size;
} }
a {
color: $gl-text-color;
}
.avatar-container, .avatar-container,
.controls { .controls {
flex: 0 0 auto; flex: 0 0 auto;
......
...@@ -35,259 +35,225 @@ ...@@ -35,259 +35,225 @@
} }
} }
.repository-view { .multi-file {
border: 1px solid $border-color; display: flex;
border-radius: $border-radius-default; height: calc(100vh - 145px);
color: $almost-black; border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
&.is-collapsed {
.ide-file-list {
max-width: 250px;
}
}
.code.white pre .hll { .file-status-icon {
background-color: $well-light-border !important; width: 10px;
height: 10px;
} }
}
.tree-content-holder { .ide-file-list {
display: -webkit-flex; flex: 1;
display: flex; overflow: scroll;
min-height: 300px;
.file {
cursor: pointer;
} }
.tree-content-holder-mini { a {
height: 100vh; color: $gl-text-color;
} }
.panel-right { th {
display: -webkit-flex; position: sticky;
display: flex; top: 0;
-webkit-flex-direction: column; }
flex-direction: column; }
width: 80%;
height: 100%;
.monaco-editor.vs { .multi-file-table-name,
.current-line { .multi-file-table-col-commit-message {
border: 0; white-space: nowrap;
background: $well-light-border; overflow: hidden;
} text-overflow: ellipsis;
max-width: 0;
}
.line-numbers { .multi-file-table-name {
cursor: pointer; width: 350px;
}
&:hover { .multi-file-table-col-commit-message {
text-decoration: underline; width: 50%;
} }
}
}
.blob-no-preview { .multi-file-edit-pane {
.vertical-center { display: flex;
justify-content: center; flex-direction: column;
width: 100%; flex: 1;
} border-left: 1px solid $white-dark;
} overflow: hidden;
}
&.blob-editor-container { .multi-file-tabs {
overflow: hidden; display: flex;
} overflow: scroll;
background-color: $white-normal;
box-shadow: inset 0 -1px $white-dark;
.blob-viewer-container { > li {
-webkit-flex: 1; position: relative;
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 { .multi-file-tab {
position: relative; @include str-truncated(150px);
flex-shrink: 0; padding: ($gl-padding / 2) ($gl-padding + 12) ($gl-padding / 2) $gl-padding;
display: flex; background-color: $gray-normal;
width: 100%; border-right: 1px solid $white-dark;
padding-left: 0; border-bottom: 1px solid $white-dark;
margin-bottom: 0; cursor: pointer;
white-space: nowrap;
overflow-y: hidden; &.active {
overflow-x: auto; background-color: $white-light;
border-bottom-color: $white-light;
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 { .multi-file-tab-close {
background-color: $white-light; position: absolute;
padding: 5px 10px; right: 8px;
border-top: 1px solid $white-normal; 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;
}
#binary-viewer { &.modified {
height: 80vh; color: $indigo-700;
overflow: auto; }
margin: 0; }
.blob-viewer { .multi-file-edit-pane-content {
padding-top: 20px; flex: 1;
padding-left: 20px; height: 0;
} }
.binary-unknown { .multi-file-editor-btn-group {
text-align: center; padding: $grid-size;
padding-top: 100px; border-top: 1px solid $white-dark;
background: $gray-light; }
height: 100%;
font-size: 17px; // Not great, but this is to deal with our current output
.multi-file-preview-holder {
span { height: 100%;
display: block; overflow: scroll;
}
} .blob-viewer {
} height: 100%;
} }
#commit-area { .file-content.code {
background: $gray-light; display: flex;
padding: 20px;
.help-block { i {
padding-top: 7px; margin-left: -10px;
margin-top: 0;
} }
} }
#view-toggler { .line-numbers {
height: 41px; min-width: 50px;
position: relative;
display: block;
border-bottom: 1px solid $white-normal;
background: $white-light;
margin-top: -5px;
} }
#binary-viewer { .file-content,
img { .line-numbers,
max-width: 100%; .blob-content,
} .code {
min-height: 100%;
} }
}
#sidebar { .multi-file-commit-panel {
flex: 1; display: flex;
height: 100%; 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 { .multi-file-commit-panel-section {
width: 20%; display: flex;
border-right: 1px solid $white-normal; flex-direction: column;
overflow: auto; flex: 1;
} }
.table { .multi-file-commit-panel-header {
margin-bottom: 0; display: flex;
} align-items: center;
padding: 0 0 12px;
margin-bottom: 12px;
border-bottom: 1px solid $white-dark;
tr { &.is-collapsed {
.repo-file-options { border-bottom: 1px solid $white-dark;
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 { svg {
cursor: pointer; margin-left: auto;
margin-right: auto;
} }
}
}
a { .multi-file-commit-panel-collapse-btn {
@include str-truncated(250px); padding-top: 0;
color: $almost-black; 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;
} }
.file-status-icon { .file-status-icon {
...@@ -298,14 +264,59 @@ ...@@ -298,14 +264,59 @@
} }
.render-error { .multi-file-commit-list-path {
min-height: calc(100vh - 62px); @include str-truncated(100%);
}
.multi-file-commit-form {
padding-top: 12px;
border-top: 1px solid $white-dark;
}
p { .multi-file-commit-fieldset {
width: 100%; display: flex;
align-items: center;
padding-bottom: 12px;
.btn {
flex: 1;
} }
} }
.multi-file-table-col-name { .multi-file-commit-message.form-control {
width: 350px; height: 80px;
resize: none;
}
.dirty-diff {
// !important need to override monaco inline style
width: 4px !important;
left: 0 !important;
&-modified {
background-color: $blue-500;
}
&-added {
background-color: $green-600;
}
&-removed {
height: 0 !important;
width: 0 !important;
bottom: -2px;
border-style: solid;
border-width: 5px;
border-color: transparent transparent transparent $red-500;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 100px;
height: 1px;
background-color: rgba($red-500, .5);
}
}
} }
...@@ -161,7 +161,8 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -161,7 +161,8 @@ class Projects::IssuesController < Projects::ApplicationController
end end
def create_merge_request 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 if result[:status] == :success
render json: MergeRequestCreateSerializer.new.represent(result[:merge_request]) render json: MergeRequestCreateSerializer.new.represent(result[:merge_request])
......
...@@ -67,7 +67,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap ...@@ -67,7 +67,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
if params[:ref].present? if params[:ref].present?
@ref = params[:ref] @ref = params[:ref]
@commit = @repository.commit("refs/heads/#{@ref}") @commit = @repository.commit(Gitlab::Git::BRANCH_REF_PREFIX + @ref)
end end
render layout: false render layout: false
...@@ -78,7 +78,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap ...@@ -78,7 +78,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
if params[:ref].present? if params[:ref].present?
@ref = params[:ref] @ref = params[:ref]
@commit = @target_project.commit("refs/heads/#{@ref}") @commit = @target_project.commit(Gitlab::Git::BRANCH_REF_PREFIX + @ref)
end end
render layout: false render layout: false
......
...@@ -27,7 +27,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic ...@@ -27,7 +27,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
@merge_request.merge_request_diff @merge_request.merge_request_diff
end 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 } @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present? if params[:start_sha].present?
......
...@@ -6,11 +6,19 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController ...@@ -6,11 +6,19 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
end end
def update def update
if @project.update(update_params) Projects::UpdateService.new(project, current_user, update_params).tap do |service|
flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated." if service.execute
redirect_to project_settings_ci_cd_path(@project) flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
else
render 'show' 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
end end
...@@ -21,6 +29,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController ...@@ -21,6 +29,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
:runners_token, :builds_enabled, :build_allow_git_fetch, :runners_token, :builds_enabled, :build_allow_git_fetch,
:build_timeout_in_minutes, :build_coverage_regex, :public_builds, :build_timeout_in_minutes, :build_coverage_regex, :public_builds,
:auto_cancel_pending_pipelines, :ci_config_path, :auto_cancel_pending_pipelines, :ci_config_path,
:run_auto_devops_pipeline_implicit, :run_auto_devops_pipeline_explicit,
auto_devops_attributes: [:id, :domain, :enabled] auto_devops_attributes: [:id, :domain, :enabled]
) )
end end
......
...@@ -104,8 +104,7 @@ class NotesFinder ...@@ -104,8 +104,7 @@ class NotesFinder
query = @params[:search] query = @params[:search]
return notes unless query return notes unless query
pattern = "%#{query}%" notes.search(query)
notes.where(Note.arel_table[:note].matches(pattern))
end end
# Notes changed since last fetch # Notes changed since last fetch
......
...@@ -31,9 +31,9 @@ module ApplicationSettingsHelper ...@@ -31,9 +31,9 @@ module ApplicationSettingsHelper
def enabled_project_button(project, protocol) def enabled_project_button(project, protocol)
case protocol case protocol
when 'ssh' when 'ssh'
ssh_clone_button(project, 'bottom', append_link: false) ssh_clone_button(project, append_link: false)
else else
http_clone_button(project, 'bottom', append_link: false) http_clone_button(project, append_link: false)
end end
end end
......
...@@ -8,6 +8,22 @@ module AutoDevopsHelper ...@@ -8,6 +8,22 @@ module AutoDevopsHelper
!project.ci_service !project.ci_service
end 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) def auto_devops_warning_message(project)
missing_domain = !project.auto_devops&.has_domain? missing_domain = !project.auto_devops&.has_domain?
missing_service = !project.kubernetes_service&.active? missing_service = !project.kubernetes_service&.active?
......
...@@ -56,44 +56,41 @@ module ButtonHelper ...@@ -56,44 +56,41 @@ module ButtonHelper
end end
end end
def http_clone_button(project, placement = 'right', append_link: true) def http_clone_button(project, append_link: true)
klass = 'http-selector'
klass << ' has-tooltip' if current_user.try(:require_extra_setup_for_git_auth?)
protocol = gitlab_config.protocol.upcase protocol = gitlab_config.protocol.upcase
dropdown_description = http_dropdown_description(protocol)
append_url = project.http_url_to_repo if append_link
geo_url = geo_primary_http_url_to_repo(project) if Gitlab::Geo.secondary?
tooltip_title = dropdown_item_with_description(protocol, dropdown_description, href: append_url, geo_url: geo_url)
if current_user.try(:require_password_creation_for_git?) end
_("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, def http_dropdown_description(protocol)
class: klass, if current_user.try(:require_password_creation_for_git?)
href: (project.http_url_to_repo if append_link), _("Set a password on your account to pull or push via %{protocol}.") % { protocol: protocol }
data: { else
html: true, _("Create a personal access token on your account to pull or push via %{protocol}.") % { protocol: protocol }
placement: placement, end
container: 'body',
title: tooltip_title,
primary_url: (geo_primary_http_url_to_repo(project) if Gitlab::Geo.secondary?)
}
end end
def ssh_clone_button(project, placement = 'right', append_link: true) def ssh_clone_button(project, append_link: true)
klass = 'ssh-selector' 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?)
klass << ' has-tooltip' if current_user.try(:require_ssh_key?) append_url = project.ssh_url_to_repo if append_link
geo_url = geo_primary_ssh_url_to_repo(project) if Gitlab::Geo.secondary?
content_tag (append_link ? :a : :span), 'SSH', dropdown_item_with_description('SSH', dropdown_description, href: append_url, geo_url: geo_url)
class: klass, end
href: (project.ssh_url_to_repo if append_link),
def dropdown_item_with_description(title, description, href: nil, geo_url: 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),
data: { data: {
html: true, primary_url: (geo_url if geo_url)
placement: placement,
container: 'body',
title: _('Add an SSH key to your profile to pull or push via SSH.'),
primary_url: (geo_primary_ssh_url_to_repo(project) if Gitlab::Geo.secondary?)
} }
end end
......
...@@ -343,13 +343,13 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -343,13 +343,13 @@ class ApplicationSetting < ActiveRecord::Base
user_default_external: false, user_default_external: false,
polling_interval_multiplier: 1, 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,
slack_app_enabled: false, slack_app_enabled: false,
slack_app_id: nil, slack_app_id: nil,
slack_app_secret: nil, slack_app_secret: nil,
slack_app_verification_token: nil, slack_app_verification_token: nil
gitaly_timeout_fast: 10,
gitaly_timeout_medium: 30,
gitaly_timeout_default: 55
} }
end end
......
module Ci module Ci
class Runner < ActiveRecord::Base class Runner < ActiveRecord::Base
extend Gitlab::Ci::Model extend Gitlab::Ci::Model
include Gitlab::SQL::Pattern
prepend EE::Ci::Runner prepend EE::Ci::Runner
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
...@@ -60,10 +61,7 @@ module Ci ...@@ -60,10 +61,7 @@ module Ci
# #
# Returns an ActiveRecord::Relation. # Returns an ActiveRecord::Relation.
def self.search(query) def self.search(query)
t = arel_table fuzzy_search(query, [:token, :description])
pattern = "%#{query}%"
where(t[:token].matches(pattern).or(t[:description].matches(pattern)))
end end
def self.contact_time_deadline def self.contact_time_deadline
......
...@@ -121,9 +121,7 @@ module Issuable ...@@ -121,9 +121,7 @@ module Issuable
# #
# Returns an ActiveRecord::Relation. # Returns an ActiveRecord::Relation.
def search(query) def search(query)
title = to_fuzzy_arel(:title, query) fuzzy_search(query, [:title])
where(title)
end end
# Searches for records with a matching title or description. # Searches for records with a matching title or description.
...@@ -134,10 +132,7 @@ module Issuable ...@@ -134,10 +132,7 @@ module Issuable
# #
# Returns an ActiveRecord::Relation. # Returns an ActiveRecord::Relation.
def full_search(query) def full_search(query)
title = to_fuzzy_arel(:title, query) fuzzy_search(query, [:title, :description])
description = to_fuzzy_arel(:description, query)
where(title&.or(description))
end end
def sort(method, excluded_labels: []) def sort(method, excluded_labels: [])
......
class Email < ActiveRecord::Base class Email < ActiveRecord::Base
include Sortable include Sortable
include Gitlab::SQL::Pattern
belongs_to :user belongs_to :user
......
...@@ -8,19 +8,17 @@ class GeoNode < ActiveRecord::Base ...@@ -8,19 +8,17 @@ class GeoNode < ActiveRecord::Base
has_many :namespaces, through: :geo_node_namespace_links has_many :namespaces, through: :geo_node_namespace_links
has_one :status, class_name: 'GeoNodeStatus' has_one :status, class_name: 'GeoNodeStatus'
default_values schema: lambda { Gitlab.config.gitlab.protocol }, default_values url: ->(record) { record.class.current_node_url },
host: lambda { Gitlab.config.gitlab.host },
port: lambda { Gitlab.config.gitlab.port },
relative_url_root: lambda { Gitlab.config.gitlab.relative_url_root },
primary: false, primary: false,
clone_protocol: 'http' clone_protocol: 'http'
accepts_nested_attributes_for :geo_node_key accepts_nested_attributes_for :geo_node_key
validates :host, host: true, presence: true, uniqueness: { case_sensitive: false, scope: :port } validates :url, presence: true, uniqueness: { case_sensitive: false }
validate :check_url_is_valid
validates :primary, uniqueness: { message: 'node already exists' }, if: :primary validates :primary, uniqueness: { message: 'node already exists' }, if: :primary
validates :schema, inclusion: %w(http https)
validates :relative_url_root, length: { minimum: 0, allow_nil: false }
validates :access_key, presence: true validates :access_key, presence: true
validates :encrypted_secret_access_key, presence: true validates :encrypted_secret_access_key, presence: true
validates :clone_protocol, presence: true, inclusion: %w(ssh http) validates :clone_protocol, presence: true, inclusion: %w(ssh http)
...@@ -35,16 +33,33 @@ class GeoNode < ActiveRecord::Base ...@@ -35,16 +33,33 @@ class GeoNode < ActiveRecord::Base
before_validation :ensure_access_keys! before_validation :ensure_access_keys!
scope :with_url_prefix, ->(prefix) { where('url LIKE ?', "#{prefix}%") }
attr_encrypted :secret_access_key, attr_encrypted :secret_access_key,
key: Gitlab::Application.secrets.db_key_base, key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-gcm', algorithm: 'aes-256-gcm',
mode: :per_attribute_iv, mode: :per_attribute_iv,
encode: true encode: true
class << self
def current_node_url
RequestStore.fetch('geo_node:current_node_url') do
cfg = Gitlab.config.gitlab
uri = URI.parse("#{cfg.protocol}://#{cfg.host}:#{cfg.port}#{cfg.relative_url_root}")
uri.path += '/' unless uri.path.end_with?('/')
uri.to_s
end
end
def current_node
GeoNode.find_by(url: current_node_url)
end
end
def current? def current?
host == Gitlab.config.gitlab.host && self.class.current_node_url == url
port == Gitlab.config.gitlab.port &&
relative_url_root == Gitlab.config.gitlab.relative_url_root
end end
def secondary? def secondary?
...@@ -55,24 +70,23 @@ class GeoNode < ActiveRecord::Base ...@@ -55,24 +70,23 @@ class GeoNode < ActiveRecord::Base
secondary? && clone_protocol == 'ssh' secondary? && clone_protocol == 'ssh'
end end
def uri def url
if relative_url_root value = read_attribute(:url)
relative_url = relative_url_root.starts_with?('/') ? relative_url_root : "/#{relative_url_root}" value += '/' if value.present? && !value.end_with?('/')
end
URI.parse(URI::Generic.build(scheme: schema, host: host, port: port, path: relative_url).normalize.to_s) value
end end
def url def url=(value)
uri.to_s value += '/' if value.present? && !value.end_with?('/')
write_attribute(:url, value)
@uri = nil
end end
def url=(new_url) def uri
new_uri = URI.parse(new_url) @uri ||= URI.parse(url) if url.present?
self.schema = new_uri.scheme
self.host = new_uri.host
self.port = new_uri.port
self.relative_url_root = new_uri.path != '/' ? new_uri.path : ''
end end
def geo_transfers_url(file_type, file_id) def geo_transfers_url(file_type, file_id)
...@@ -203,7 +217,7 @@ class GeoNode < ActiveRecord::Base ...@@ -203,7 +217,7 @@ class GeoNode < ActiveRecord::Base
private private
def geo_api_url(suffix) def geo_api_url(suffix)
URI.join(uri, "#{uri.path}/", "api/#{API::API.version}/geo/#{suffix}").to_s URI.join(uri, "#{uri.path}", "api/#{API::API.version}/geo/#{suffix}").to_s
end end
def ensure_access_keys! def ensure_access_keys!
...@@ -216,11 +230,7 @@ class GeoNode < ActiveRecord::Base ...@@ -216,11 +230,7 @@ class GeoNode < ActiveRecord::Base
end end
def url_helper_args def url_helper_args
if relative_url_root { protocol: uri.scheme, host: uri.host, port: uri.port, script_name: uri.path }
relative_url = relative_url_root.starts_with?('/') ? relative_url_root : "/#{relative_url_root}"
end
{ protocol: schema, host: host, port: port, script_name: relative_url }
end end
def build_dependents def build_dependents
...@@ -246,13 +256,19 @@ class GeoNode < ActiveRecord::Base ...@@ -246,13 +256,19 @@ class GeoNode < ActiveRecord::Base
# Prevent locking yourself out # Prevent locking yourself out
def check_not_adding_primary_as_secondary def check_not_adding_primary_as_secondary
if host == Gitlab.config.gitlab.host && if url == self.class.current_node_url
port == Gitlab.config.gitlab.port &&
relative_url_root == Gitlab.config.gitlab.relative_url_root
errors.add(:base, 'Current node must be the primary node or you will be locking yourself out') errors.add(:base, 'Current node must be the primary node or you will be locking yourself out')
end end
end end
def check_url_is_valid
if uri.present? && !%w[http https].include?(uri.scheme)
errors.add(:url, 'scheme must be http or https')
end
rescue URI::InvalidURIError
errors.add(:url, 'is invalid')
end
def update_clone_url def update_clone_url
self.clone_url_prefix = Gitlab.config.gitlab_shell.ssh_path_prefix self.clone_url_prefix = Gitlab.config.gitlab_shell.ssh_path_prefix
end end
......
...@@ -68,20 +68,6 @@ class Group < Namespace ...@@ -68,20 +68,6 @@ class Group < Namespace
Gitlab::Database.postgresql? Gitlab::Database.postgresql?
end 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) def sort(method)
if method == 'storage_size_desc' if method == 'storage_size_desc'
# storage_size is a virtual column so we need to # storage_size is a virtual column so we need to
......
...@@ -292,9 +292,9 @@ class MergeRequest < ActiveRecord::Base ...@@ -292,9 +292,9 @@ class MergeRequest < ActiveRecord::Base
if persisted? if persisted?
merge_request_diff.commit_shas merge_request_diff.commit_shas
elsif compare_commits elsif compare_commits
compare_commits.reverse.map(&:sha) compare_commits.to_a.reverse.map(&:sha)
else else
[] Array(diff_head_sha)
end end
end end
...@@ -373,16 +373,28 @@ class MergeRequest < ActiveRecord::Base ...@@ -373,16 +373,28 @@ class MergeRequest < ActiveRecord::Base
# We use these attributes to force these to the intended values. # We use these attributes to force these to the intended values.
attr_writer :target_branch_sha, :source_branch_sha 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 def source_branch_head
return unless source_project return unless source_project
source_branch_ref = @source_branch_sha || source_branch
source_project.repository.commit(source_branch_ref) if source_branch_ref source_project.repository.commit(source_branch_ref) if source_branch_ref
end end
def target_branch_head def target_branch_head
target_branch_ref = @target_branch_sha || target_branch target_project.repository.commit(target_branch_ref)
target_project.repository.commit(target_branch_ref) if target_branch_ref
end end
def branch_merge_base_commit def branch_merge_base_commit
...@@ -507,7 +519,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -507,7 +519,7 @@ class MergeRequest < ActiveRecord::Base
def merge_request_diff_for(diff_refs_or_sha) 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| @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] = h[diff_refs_or_sha] =
if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs) if diff_refs_or_sha.is_a?(Gitlab::Diff::DiffRefs)
diffs.find_by_diff_refs(diff_refs_or_sha) diffs.find_by_diff_refs(diff_refs_or_sha)
...@@ -932,28 +944,18 @@ class MergeRequest < ActiveRecord::Base ...@@ -932,28 +944,18 @@ class MergeRequest < ActiveRecord::Base
# Note that this could also return SHA from now dangling commits # Note that this could also return SHA from now dangling commits
# #
def all_commit_shas def all_commit_shas
if persisted? return commit_shas unless 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
column_shas = MergeRequestDiffCommit diffs_relation = merge_request_diffs
.where(merge_request_diff: diffs_relation)
.limit(10_000)
.pluck('sha')
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 MergeRequestDiffCommit
elsif compare_commits .where(merge_request_diff: diffs_relation)
compare_commits.to_a.reverse.map(&:id) .limit(10_000)
else .pluck('sha')
[diff_head_sha] .uniq
end
end end
def merge_commit def merge_commit
......
class MergeRequestDiff < ActiveRecord::Base class MergeRequestDiff < ActiveRecord::Base
include Sortable include Sortable
include Importable include Importable
include Gitlab::EncodingHelper
include ManualInverseAssociation 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 COMMITS_SAFE_SIZE = 100
# Valid types of serialized diffs allowed by Gitlab::Git::Diff ignore_column :st_commits,
VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta].freeze :st_diffs
belongs_to :merge_request belongs_to :merge_request
manual_inverse_association :merge_request, :merge_request_diff manual_inverse_association :merge_request, :merge_request_diff
...@@ -16,9 +16,6 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -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_files, -> { order(:merge_request_diff_id, :relative_order) }
has_many :merge_request_diff_commits, -> { 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_machine :state, initial: :empty do
state :collected state :collected
state :overflow state :overflow
...@@ -32,6 +29,8 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -32,6 +29,8 @@ class MergeRequestDiff < ActiveRecord::Base
scope :viewable, -> { without_state(:empty) } scope :viewable, -> { without_state(:empty) }
scope :recent, -> { order(id: :desc).limit(100) }
# All diff information is collected from repository after object is created. # All diff information is collected from repository after object is created.
# It allows you to override variables like head_commit_sha before getting diff. # It allows you to override variables like head_commit_sha before getting diff.
after_create :save_git_content, unless: :importing? after_create :save_git_content, unless: :importing?
...@@ -40,14 +39,6 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -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) find_by(start_commit_sha: diff_refs.start_sha, head_commit_sha: diff_refs.head_sha, base_commit_sha: diff_refs.base_sha)
end 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 # Collect information about commits and diff from repository
# and save it to the database as serialized data # and save it to the database as serialized data
def save_git_content def save_git_content
...@@ -129,11 +120,7 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -129,11 +120,7 @@ class MergeRequestDiff < ActiveRecord::Base
end end
def commit_shas def commit_shas
if st_commits.present? merge_request_diff_commits.map(&:sha)
st_commits.map { |commit| commit[:id] }
else
merge_request_diff_commits.map(&:sha)
end
end end
def diff_refs=(new_diff_refs) def diff_refs=(new_diff_refs)
...@@ -208,34 +195,11 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -208,34 +195,11 @@ class MergeRequestDiff < ActiveRecord::Base
end end
def commits_count def commits_count
if st_commits.present? merge_request_diff_commits.size
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
end end
private 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) def create_merge_request_diff_files(diffs)
rows = diffs.map.with_index do |diff, index| rows = diffs.map.with_index do |diff, index|
diff_hash = diff.to_hash.merge( diff_hash = diff.to_hash.merge(
...@@ -259,9 +223,7 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -259,9 +223,7 @@ class MergeRequestDiff < ActiveRecord::Base
end end
def load_diffs(options) def load_diffs(options)
return Gitlab::Git::DiffCollection.new([]) unless diffs_from_database raw = merge_request_diff_files.map(&:to_hash)
raw = diffs_from_database
if paths = options[:paths] if paths = options[:paths]
raw = raw.select do |diff| raw = raw.select do |diff|
...@@ -272,22 +234,8 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -272,22 +234,8 @@ class MergeRequestDiff < ActiveRecord::Base
Gitlab::Git::DiffCollection.new(raw, options) Gitlab::Git::DiffCollection.new(raw, options)
end 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 def load_commits
commits = st_commits.presence || merge_request_diff_commits commits = merge_request_diff_commits.map { |commit| Commit.from_hash(commit.to_hash, project) }
commits = commits.map { |commit| Commit.from_hash(commit.to_hash, project) }
CommitCollection CommitCollection
.new(merge_request.source_project, commits, merge_request.source_branch) .new(merge_request.source_project, commits, merge_request.source_branch)
......
...@@ -14,6 +14,7 @@ class Milestone < ActiveRecord::Base ...@@ -14,6 +14,7 @@ class Milestone < ActiveRecord::Base
include StripAttribute include StripAttribute
include Elastic::MilestonesSearch include Elastic::MilestonesSearch
include Milestoneish include Milestoneish
include Gitlab::SQL::Pattern
include ::EE::Milestone include ::EE::Milestone
...@@ -77,10 +78,7 @@ class Milestone < ActiveRecord::Base ...@@ -77,10 +78,7 @@ class Milestone < ActiveRecord::Base
# #
# Returns an ActiveRecord::Relation. # Returns an ActiveRecord::Relation.
def search(query) def search(query)
t = arel_table fuzzy_search(query, [:title, :description])
pattern = "%#{query}%"
where(t[:title].matches(pattern).or(t[:description].matches(pattern)))
end end
def filter_by_state(milestones, state) def filter_by_state(milestones, state)
......
...@@ -10,6 +10,7 @@ class Namespace < ActiveRecord::Base ...@@ -10,6 +10,7 @@ class Namespace < ActiveRecord::Base
include Routable include Routable
include AfterCommitQueue include AfterCommitQueue
include Storage::LegacyNamespace include Storage::LegacyNamespace
include Gitlab::SQL::Pattern
# Prevent users from creating unreasonably deep level of nesting. # Prevent users from creating unreasonably deep level of nesting.
# The number 20 was taken based on maximum nesting level of # The number 20 was taken based on maximum nesting level of
...@@ -87,10 +88,7 @@ class Namespace < ActiveRecord::Base ...@@ -87,10 +88,7 @@ class Namespace < ActiveRecord::Base
# #
# Returns an ActiveRecord::Relation # Returns an ActiveRecord::Relation
def search(query) def search(query)
t = arel_table fuzzy_search(query, [:name, :path])
pattern = "%#{query}%"
where(t[:name].matches(pattern).or(t[:path].matches(pattern)))
end end
def clean_path(path) def clean_path(path)
......
...@@ -17,6 +17,7 @@ class Note < ActiveRecord::Base ...@@ -17,6 +17,7 @@ class Note < ActiveRecord::Base
include ResolvableNote include ResolvableNote
include IgnorableColumn include IgnorableColumn
include Editable include Editable
include Gitlab::SQL::Pattern
module SpecialRole module SpecialRole
FIRST_TIME_CONTRIBUTOR = :first_time_contributor FIRST_TIME_CONTRIBUTOR = :first_time_contributor
...@@ -171,6 +172,10 @@ class Note < ActiveRecord::Base ...@@ -171,6 +172,10 @@ class Note < ActiveRecord::Base
def has_special_role?(role, note) def has_special_role?(role, note)
note.special_role == role note.special_role == role
end end
def search(query)
fuzzy_search(query, [:note])
end
end end
def searchable? def searchable?
......
...@@ -425,17 +425,11 @@ class Project < ActiveRecord::Base ...@@ -425,17 +425,11 @@ class Project < ActiveRecord::Base
# #
# query - The search query as a String. # query - The search query as a String.
def search(query) def search(query)
pattern = to_pattern(query) fuzzy_search(query, [:path, :name, :description])
where(
arel_table[:path].matches(pattern)
.or(arel_table[:name].matches(pattern))
.or(arel_table[:description].matches(pattern))
)
end end
def search_by_title(query) def search_by_title(query)
non_archived.where(arel_table[:name].matches(to_pattern(query))) non_archived.fuzzy_search(query, [:name])
end end
def visibility_levels def visibility_levels
......
...@@ -10,6 +10,7 @@ class Snippet < ActiveRecord::Base ...@@ -10,6 +10,7 @@ class Snippet < ActiveRecord::Base
include Mentionable include Mentionable
include Spammable include Spammable
include Editable include Editable
include Gitlab::SQL::Pattern
extend Gitlab::CurrentSettings extend Gitlab::CurrentSettings
...@@ -136,10 +137,7 @@ class Snippet < ActiveRecord::Base ...@@ -136,10 +137,7 @@ class Snippet < ActiveRecord::Base
# #
# Returns an ActiveRecord::Relation. # Returns an ActiveRecord::Relation.
def search(query) def search(query)
t = arel_table fuzzy_search(query, [:title, :file_name])
pattern = "%#{query}%"
where(t[:title].matches(pattern).or(t[:file_name].matches(pattern)))
end end
# Searches for snippets with matching content. # Searches for snippets with matching content.
...@@ -150,10 +148,7 @@ class Snippet < ActiveRecord::Base ...@@ -150,10 +148,7 @@ class Snippet < ActiveRecord::Base
# #
# Returns an ActiveRecord::Relation. # Returns an ActiveRecord::Relation.
def search_code(query) def search_code(query)
table = Snippet.arel_table fuzzy_search(query, [:content])
pattern = "%#{query}%"
where(table[:content].matches(pattern))
end end
end end
end end
...@@ -327,9 +327,6 @@ class User < ActiveRecord::Base ...@@ -327,9 +327,6 @@ class User < ActiveRecord::Base
# #
# Returns an ActiveRecord::Relation. # Returns an ActiveRecord::Relation.
def search(query) def search(query)
table = arel_table
pattern = User.to_pattern(query)
order = <<~SQL order = <<~SQL
CASE CASE
WHEN users.name = %{query} THEN 0 WHEN users.name = %{query} THEN 0
...@@ -339,11 +336,8 @@ class User < ActiveRecord::Base ...@@ -339,11 +336,8 @@ class User < ActiveRecord::Base
END END
SQL SQL
where( fuzzy_search(query, [:name, :email, :username])
table[:name].matches(pattern) .reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
.or(table[:email].matches(pattern))
.or(table[:username].matches(pattern))
).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
end end
# searches user by given pattern # searches user by given pattern
...@@ -351,16 +345,16 @@ class User < ActiveRecord::Base ...@@ -351,16 +345,16 @@ class User < ActiveRecord::Base
# This method uses ILIKE on PostgreSQL and LIKE on MySQL. # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
def search_with_secondary_emails(query) def search_with_secondary_emails(query)
table = arel_table
email_table = Email.arel_table email_table = Email.arel_table
pattern = "%#{query}%" matched_by_emails_user_ids = email_table
matched_by_emails_user_ids = email_table.project(email_table[:user_id]).where(email_table[:email].matches(pattern)) .project(email_table[:user_id])
.where(Email.fuzzy_arel_match(:email, query))
where( where(
table[:name].matches(pattern) fuzzy_arel_match(:name, query)
.or(table[:email].matches(pattern)) .or(fuzzy_arel_match(:email, query))
.or(table[:username].matches(pattern)) .or(fuzzy_arel_match(:username, query))
.or(table[:id].in(matched_by_emails_user_ids)) .or(arel_table[:id].in(matched_by_emails_user_ids))
) )
end end
......
require 'securerandom' 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 # and return Gitlab::Git::Compare object that responds to commits and diffs
class CompareService 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_project = new_start_project
@start_branch_name = new_start_branch_name @start_ref_name = new_start_ref_name
end end
def execute(target_project, target_branch, straight: false) def execute(target_project, target_ref, straight: false)
raw_compare = target_project.repository.compare_source_branch(target_branch, start_project.repository, start_branch_name, straight: straight) 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 Compare.new(raw_compare, target_project, straight: straight) if raw_compare
end end
......
...@@ -3,6 +3,8 @@ module MergeRequests ...@@ -3,6 +3,8 @@ module MergeRequests
prepend EE::MergeRequests::BuildService prepend EE::MergeRequests::BuildService
def execute def execute
@issue_iid = params.delete(:issue_iid)
self.merge_request = MergeRequest.new(params) self.merge_request = MergeRequest.new(params)
merge_request.compare_commits = [] merge_request.compare_commits = []
merge_request.source_project = find_source_project merge_request.source_project = find_source_project
...@@ -20,7 +22,17 @@ module MergeRequests ...@@ -20,7 +22,17 @@ module MergeRequests
attr_accessor :merge_request 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 def find_source_project
return source_project if source_project.present? && can?(current_user, :read_project, source_project) return source_project if source_project.present? && can?(current_user, :read_project, source_project)
...@@ -56,10 +68,10 @@ module MergeRequests ...@@ -56,10 +68,10 @@ module MergeRequests
def compare_branches def compare_branches
compare = CompareService.new( compare = CompareService.new(
source_project, source_project,
source_branch source_branch_ref
).execute( ).execute(
target_project, target_project,
target_branch target_branch_ref
) )
if compare if compare
...@@ -109,20 +121,30 @@ module MergeRequests ...@@ -109,20 +121,30 @@ module MergeRequests
# #
def assign_title_and_description def assign_title_and_description
assign_title_and_description_from_single_commit assign_title_and_description_from_single_commit
assign_title_from_issue assign_title_from_issue
merge_request.title ||= source_branch.titleize.humanize merge_request.title ||= source_branch.titleize.humanize
merge_request.title = wip_title if compare_commits.empty? merge_request.title = wip_title if compare_commits.empty?
append_closes_description append_closes_description
end 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.description = closes_issue
end
end
def assign_title_and_description_from_single_commit def assign_title_and_description_from_single_commit
commits = compare_commits commits = compare_commits
return unless commits && commits.count == 1 return unless commits&.count == 1
commit = commits.first commit = commits.first
merge_request.title ||= commit.title merge_request.title ||= commit.title
...@@ -132,36 +154,19 @@ module MergeRequests ...@@ -132,36 +154,19 @@ module MergeRequests
def assign_title_from_issue def assign_title_from_issue
return unless issue return unless issue
case issue merge_request.title =
when Issue case issue
merge_request.title ||= "Resolve \"#{issue.title}\"" when Issue then "Resolve \"#{issue.title}\""
when ExternalIssue when ExternalIssue then "Resolve #{issue.title}"
merge_request.title ||= "Resolve #{issue.title}" end
end
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.description = closes_issue
end
end end
def issue_iid def issue_iid
return @issue_iid if defined?(@issue_iid) @issue_iid ||= source_branch.match(/\A(\d+)-/).try(:[], 1)
@issue_iid = source_branch[/\A(\d+)-/, 1]
end end
def issue def issue
return @issue if defined?(@issue) @issue ||= target_project.get_issue(issue_iid, current_user)
@issue = target_project.get_issue(issue_iid, current_user)
end end
end end
end end
module MergeRequests module MergeRequests
class CreateFromIssueService < MergeRequests::CreateService 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 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? params[:label_ids] = issue.label_ids if issue.label_ids.any?
...@@ -21,20 +32,16 @@ module MergeRequests ...@@ -21,20 +32,16 @@ module MergeRequests
private private
def issue_iid
@isssue_iid ||= params.delete(:issue_iid)
end
def issue 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 end
def branch_name def branch_name
@branch_name ||= issue.to_branch_name @branch ||= @branch_name || issue.to_branch_name
end end
def ref def ref
project.default_branch || 'master' @ref || project.default_branch || 'master'
end end
def merge_request def merge_request
...@@ -43,6 +50,7 @@ module MergeRequests ...@@ -43,6 +50,7 @@ module MergeRequests
def merge_request_params def merge_request_params
{ {
issue_iid: @issue_iid,
source_project_id: project.id, source_project_id: project.id,
source_branch: branch_name, source_branch: branch_name,
target_project_id: project.id, target_project_id: project.id,
......
...@@ -25,7 +25,7 @@ module Projects ...@@ -25,7 +25,7 @@ module Projects
return error("Could not set the default branch") unless project.change_head(params[:default_branch]) return error("Could not set the default branch") unless project.change_head(params[:default_branch])
end end
if project.update_attributes(params.except(:default_branch)) if project.update_attributes(update_params)
if project.previous_changes.include?('path') if project.previous_changes.include?('path')
project.rename_repo project.rename_repo
else else
...@@ -41,13 +41,14 @@ module Projects ...@@ -41,13 +41,14 @@ module Projects
end end
end end
private def run_auto_devops_pipeline?
params.dig(:run_auto_devops_pipeline_explicit) == 'true' || params.dig(:run_auto_devops_pipeline_implicit) == 'true'
end
def changing_storage_size? private
new_repository_storage = params[:repository_storage]
new_repository_storage && project.repository.exists? && def update_params
can?(current_user, :change_repository_storage, project) params.except(:default_branch, :run_auto_devops_pipeline_explicit, :run_auto_devops_pipeline_implicit)
end end
def renaming_project_with_container_registry_tags? def renaming_project_with_container_registry_tags?
......
...@@ -5,7 +5,12 @@ xml.entry do ...@@ -5,7 +5,12 @@ xml.entry do
xml.link href: event_feed_url(event) xml.link href: event_feed_url(event)
xml.title truncate(event_feed_title(event), length: 80) xml.title truncate(event_feed_title(event), length: 80)
xml.updated event.updated_at.xmlschema 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.author do
xml.username event.author_username xml.username event.author_username
......
.flash-container.flash-container-page .flash-container.flash-container-page
- if alert -# We currently only support `alert`, `notice`, `success`
.flash-alert - flash.each do |key, value|
%div{ class: "flash-#{key}" }
%div{ class: (container_class) } %div{ class: (container_class) }
%span= alert %span= value
- elsif notice
.flash-notice
%div{ class: (container_class) }
%span= notice
...@@ -67,8 +67,8 @@ ...@@ -67,8 +67,8 @@
- if @commit.last_pipeline - if @commit.last_pipeline
- last_pipeline = @commit.last_pipeline - last_pipeline = @commit.last_pipeline
.well-segment.pipeline-info .well-segment.pipeline-info
.status-icon-container{ class: "ci-status-icon-#{last_pipeline.status}" } .status-icon-container
= link_to project_pipeline_path(@project, last_pipeline.id) do = link_to project_pipeline_path(@project, last_pipeline.id), class: "ci-status-icon-#{last_pipeline.status}" do
= ci_icon_for_status(last_pipeline.status) = ci_icon_for_status(last_pipeline.status)
#{ _('Pipeline') } #{ _('Pipeline') }
= link_to "##{last_pipeline.id}", project_pipeline_path(@project, last_pipeline.id) = link_to "##{last_pipeline.id}", project_pipeline_path(@project, last_pipeline.id)
......
...@@ -13,5 +13,5 @@ ...@@ -13,5 +13,5 @@
quick_actions_docs_path: help_page_path('user/project/quick_actions'), quick_actions_docs_path: help_page_path('user/project/quick_actions'),
notes_path: notes_url, notes_path: notes_url,
last_fetched_at: Time.now.to_i, last_fetched_at: Time.now.to_i,
issue_data: serialize_issuable(@issue), noteable_data: serialize_issuable(@issue),
current_user_data: UserSerializer.new.represent(current_user).to_json } } current_user_data: UserSerializer.new.represent(current_user).to_json } }
- can_create_merge_request = can?(current_user, :create_merge_request, @project) - can_create_merge_request = can?(current_user, :create_merge_request, @project)
- data_action = can_create_merge_request ? 'create-mr' : 'create-branch' - 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) - 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 .btn-group.unavailable
%button.btn.btn-grouped{ type: 'button', disabled: 'disabled' } %button.btn.btn-grouped{ type: 'button', disabled: 'disabled' }
= icon('spinner', class: 'fa-spin') = icon('spinner', class: 'fa-spin')
%span.text %span.text
Checking branch availability… Checking branch availability…
.btn-group.available.hide .btn-group.available.hide
%input.btn.js-create-merge-request.btn-inverted.btn-success{ type: 'button', value: value, data: { action: data_action } } %button.btn.js-create-merge-request.btn-default{ type: 'button', 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' } } = 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') = 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 - if can_create_merge_request
%li.droplab-item-selected{ role: 'button', data: { value: 'create-mr', 'text' => 'Create a merge request' } } %li.create-item.droplab-item-selected.droplab-item-ignore-hiding{ role: 'button', data: { value: 'create-mr', text: 'Create merge request' } }
.menu-item .menu-item.droplab-item-ignore-hiding
.icon-container .icon-container.droplab-item-ignore-hiding= icon('check')
= icon('check') .description.droplab-item-ignore-hiding Create merge request and branch
.description
%strong Create a merge request %li.create-item.droplab-item-ignore-hiding{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', text: 'Create branch' } }
%span .menu-item.droplab-item-ignore-hiding
Creates a merge request named after this issue, with source branch created from '#{@project.default_branch}'. .icon-container.droplab-item-ignore-hiding= icon('check')
%li.divider.droplab-item-ignore .description.droplab-item-ignore-hiding Create branch
%li{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', 'text' => 'Create a branch' } } %li.divider
.menu-item
.icon-container %li.droplab-item-ignore
= icon('check') Branch name
.description %input.js-branch-name.form-control.droplab-item-ignore{ type: 'text', placeholder: "#{@issue.to_branch_name}", value: "#{@issue.to_branch_name}" }
%strong Create a branch %span.js-branch-message.branch-message.droplab-item-ignore
%span
Creates a branch named after this issue, from '#{@project.default_branch}'. %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 @@ ...@@ -13,29 +13,39 @@
%p.settings-message.text-center %p.settings-message.text-center
= message.html_safe = message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form| = f.fields_for :auto_devops_attributes, @auto_devops do |form|
.radio .radio.js-auto-devops-enable-radio-wrapper
= form.label :enabled_true do = 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 %strong Enable Auto DevOps
%br %br
%span.descr %span.descr
The Auto DevOps pipeline configuration will be used when there is no <code>.gitlab-ci.yml</code> in the project. 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.label :enabled_false do
= form.radio_button :enabled, 'false' = form.radio_button :enabled, 'false', class: 'js-auto-devops-enable-radio'
%strong Disable Auto DevOps %strong Disable Auto DevOps
%br %br
%span.descr %span.descr
An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continuous Integration and Delivery. An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continuous Integration and Delivery.
.radio .radio.js-auto-devops-enable-radio-wrapper
= form.label :enabled_nil do = form.label :enabled_ do
= form.radio_button :enabled, '' = form.radio_button :enabled, '', class: 'js-auto-devops-enable-radio'
%strong Instance default (#{current_application_settings.auto_devops_enabled? ? 'enabled' : 'disabled'}) %strong Instance default (#{current_application_settings.auto_devops_enabled? ? 'enabled' : 'disabled'})
%br %br
%span.descr %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>. 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 %p
You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages. 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' = form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
......
...@@ -11,6 +11,6 @@ ...@@ -11,6 +11,6 @@
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'repo' = 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/last_push'
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id) = render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
%span %span
= enabled_project_button(project, enabled_protocol) = enabled_project_button(project, enabled_protocol)
- else - else
%a#clone-dropdown.clone-dropdown-btn.btn{ href: '#', data: { toggle: 'dropdown' } } %a#clone-dropdown.btn.clone-dropdown-btn{ href: '#', data: { toggle: 'dropdown' } }
%span %span
= default_clone_protocol.upcase = default_clone_protocol.upcase
= icon('caret-down') = icon('caret-down')
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
= project_icon(project, alt: '', class: 'avatar project-avatar s40') = project_icon(project, alt: '', class: 'avatar project-avatar s40')
.project-details .project-details
%h3.prepend-top-0.append-bottom-0 %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.project-full-name
%span.namespace-name %span.namespace-name
- if project.namespace && !skip_namespace - if project.namespace && !skip_namespace
......
- @no_container = true;
#repo{ data: { root: @path.empty?.to_s, #repo{ data: { root: @path.empty?.to_s,
root_url: project_tree_path(project), root_url: project_tree_path(project),
url: content_url, 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
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.
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