Commit ccafe1b0 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce-to-ee' into 'master'

CE upstream: Thursday

Closes gitlab-ce#29192, #1381, and gitlab-com/infrastructure#1139

See merge request !1398
parents d7ac635b 517bbbbe
......@@ -20,6 +20,7 @@ gem 'rugged', '~> 0.24.0'
# Authentication libraries
gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0'
gem 'doorkeeper-openid_connect', '~> 1.1.0'
gem 'omniauth', '~> 1.4.2'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6'
......
......@@ -86,6 +86,7 @@ GEM
better_errors (1.0.1)
coderay (>= 1.0.0)
erubis (>= 2.6.6)
bindata (2.3.5)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootstrap-sass (3.3.6)
......@@ -175,6 +176,9 @@ GEM
unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.2.0)
railties (>= 4.2)
doorkeeper-openid_connect (1.1.2)
doorkeeper (~> 4.0)
json-jwt (~> 1.6)
dropzonejs-rails (0.7.2)
rails (> 3.1)
elasticsearch (5.0.3)
......@@ -412,6 +416,12 @@ GEM
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (1.8.6)
json-jwt (1.7.1)
activesupport
bindata
multi_json (>= 1.3)
securecompare
url_safe_base64
json-schema (2.6.2)
addressable (~> 2.3.8)
jwt (1.5.6)
......@@ -720,6 +730,7 @@ GEM
scss_lint (0.47.1)
rake (>= 0.9, < 11)
sass (~> 3.4.15)
securecompare (1.0.0)
seed-fu (2.3.6)
activerecord (>= 3.1)
activesupport (>= 3.1)
......@@ -825,6 +836,7 @@ GEM
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uniform_notifier (1.10.0)
url_safe_base64 (0.2.2)
validates_hostname (1.0.6)
activerecord (>= 3.0)
activesupport (>= 3.0)
......@@ -903,6 +915,7 @@ DEPENDENCIES
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
doorkeeper (~> 4.2.0)
doorkeeper-openid_connect (~> 1.1.0)
dropzonejs-rails (~> 0.7.1)
elasticsearch-api (= 5.0.3)
elasticsearch-model (~> 0.1.9)
......
/* global CommentsStore Cookies notes */
import Vue from 'vue';
import collapseIcon from '../icons/collapse_icon.svg';
(() => {
const DiffNoteAvatars = Vue.extend({
props: ['discussionId'],
data() {
return {
isVisible: false,
lineType: '',
storeState: CommentsStore.state,
shownAvatars: 3,
collapseIcon,
};
},
template: `
<div class="diff-comment-avatar-holders"
v-show="notesCount !== 0">
<div v-if="!isVisible">
<img v-for="note in notesSubset"
class="avatar diff-comment-avatar has-tooltip js-diff-comment-avatar"
width="19"
height="19"
role="button"
data-container="body"
data-placement="top"
:data-line-type="lineType"
:title="note.authorName + ': ' + note.noteTruncated"
:src="note.authorAvatar"
@click="clickedAvatar($event)" />
<span v-if="notesCount > shownAvatars"
class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
data-container="body"
data-placement="top"
ref="extraComments"
role="button"
:data-line-type="lineType"
:title="extraNotesTitle"
@click="clickedAvatar($event)">{{ moreText }}</span>
</div>
<button class="diff-notes-collapse js-diff-comment-avatar"
type="button"
aria-label="Show comments"
:data-line-type="lineType"
@click="clickedAvatar($event)"
v-if="isVisible"
v-html="collapseIcon">
</button>
</div>
`,
mounted() {
this.$nextTick(() => {
this.addNoCommentClass();
this.setDiscussionVisible();
this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new';
});
$(document).on('toggle.comments', () => {
this.$nextTick(() => {
this.setDiscussionVisible();
});
});
},
destroyed() {
$(document).off('toggle.comments');
},
watch: {
storeState: {
handler() {
this.$nextTick(() => {
$('.has-tooltip', this.$el).tooltip('fixTitle');
// We need to add/remove a class to an element that is outside the Vue instance
this.addNoCommentClass();
});
},
deep: true,
},
},
computed: {
notesSubset() {
let notes = [];
if (this.discussion) {
notes = Object.keys(this.discussion.notes)
.slice(0, this.shownAvatars)
.map(noteId => this.discussion.notes[noteId]);
}
return notes;
},
extraNotesTitle() {
if (this.discussion) {
const extra = this.discussion.notesCount() - this.shownAvatars;
return `${extra} more comment${extra > 1 ? 's' : ''}`;
}
return '';
},
discussion() {
return this.storeState[this.discussionId];
},
notesCount() {
if (this.discussion) {
return this.discussion.notesCount();
}
return 0;
},
moreText() {
const plusSign = this.notesCount < 100 ? '+' : '';
return `${plusSign}${this.notesCount - this.shownAvatars}`;
},
},
methods: {
clickedAvatar(e) {
notes.addDiffNote(e);
// Toggle the active state of the toggle all button
this.toggleDiscussionsToggleState();
this.$nextTick(() => {
this.setDiscussionVisible();
$('.has-tooltip', this.$el).tooltip('fixTitle');
$('.has-tooltip', this.$el).tooltip('hide');
});
},
addNoCommentClass() {
const notesCount = this.notesCount;
$(this.$el).closest('.js-avatar-container')
.toggleClass('js-no-comment-btn', notesCount > 0)
.nextUntil('.js-avatar-container')
.toggleClass('js-no-comment-btn', notesCount > 0);
},
toggleDiscussionsToggleState() {
const $notesHolders = $(this.$el).closest('.code').find('.notes_holder');
const $visibleNotesHolders = $notesHolders.filter(':visible');
const $toggleDiffCommentsBtn = $(this.$el).closest('.diff-file').find('.js-toggle-diff-comments');
$toggleDiffCommentsBtn.toggleClass('active', $notesHolders.length === $visibleNotesHolders.length);
},
setDiscussionVisible() {
this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible');
},
},
});
Vue.component('diff-note-avatars', DiffNoteAvatars);
})();
......@@ -11,7 +11,10 @@ const Vue = require('vue');
discussionId: String,
resolved: Boolean,
canResolve: Boolean,
resolvedBy: String
resolvedBy: String,
authorName: String,
authorAvatar: String,
noteTruncated: String,
},
data: function () {
return {
......@@ -98,7 +101,16 @@ const Vue = require('vue');
CommentsStore.delete(this.discussionId, this.noteId);
},
created: function () {
CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy);
CommentsStore.create({
discussionId: this.discussionId,
noteId: this.noteId,
canResolve: this.canResolve,
resolved: this.resolved,
resolvedBy: this.resolvedBy,
authorName: this.authorName,
authorAvatar: this.authorAvatar,
noteTruncated: this.noteTruncated,
});
this.note = this.discussion.getNote(this.noteId);
}
......
......@@ -14,6 +14,7 @@ require('./components/jump_to_discussion');
require('./components/resolve_btn');
require('./components/resolve_count');
require('./components/resolve_discussion_btn');
require('./components/diff_note_avatars');
$(() => {
const projectPath = document.querySelector('.merge-request').dataset.projectPath;
......@@ -25,6 +26,15 @@ $(() => {
window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath);
gl.diffNotesCompileComponents = () => {
$('diff-note-avatars').each(function () {
const tmp = Vue.extend({
template: $(this).get(0).outerHTML
});
const tmpApp = new tmp().$mount();
$(this).replaceWith(tmpApp.$el);
});
const $components = $(COMPONENT_SELECTOR).filter(function () {
return $(this).closest('resolve-count').length !== 1;
});
......
<svg width="11" height="11" viewBox="0 0 9 13"><path d="M2.57568253,6.49866948 C2.50548852,6.57199715 2.44637866,6.59708255 2.39835118,6.57392645 C2.3503237,6.55077034 2.32631032,6.48902165 2.32631032,6.38867852 L2.32631032,-2.13272614 C2.32631032,-2.23306927 2.3503237,-2.29481796 2.39835118,-2.31797406 C2.44637866,-2.34113017 2.50548852,-2.31604477 2.57568253,-2.24271709 L6.51022184,1.86747129 C6.53977721,1.8983461 6.56379059,1.93500939 6.5822627,1.97746225 L6.5822627,2.27849013 C6.56379059,2.31708364 6.53977721,2.35374693 6.51022184,2.38848109 L2.57568253,6.49866948 Z" transform="translate(4.454287, 2.127976) rotate(90.000000) translate(-4.454287, -2.127976) "></path><path d="M3.74312342,2.09553332 C3.74312342,1.99519019 3.77821989,1.9083561 3.8484139,1.83502843 C3.91860791,1.76170075 4.00173115,1.72503747 4.09778611,1.72503747 L4.80711151,1.72503747 C4.90316647,1.72503747 4.98628971,1.76170075 5.05648372,1.83502843 C5.12667773,1.9083561 5.16177421,1.99519019 5.16177421,2.09553332 L5.16177421,10.2464421 C5.16177421,10.3467853 5.12667773,10.4336194 5.05648372,10.506947 C4.98628971,10.5802747 4.90316647,10.616938 4.80711151,10.616938 L4.09778611,10.616938 C4.00173115,10.616938 3.91860791,10.5802747 3.8484139,10.506947 C3.77821989,10.4336194 3.74312342,10.3467853 3.74312342,10.2464421 L3.74312342,2.09553332 Z" transform="translate(4.452449, 6.170988) rotate(-90.000000) translate(-4.452449, -6.170988) "></path><path d="M2.57568253,14.6236695 C2.50548852,14.6969971 2.44637866,14.7220826 2.39835118,14.6989264 C2.3503237,14.6757703 2.32631032,14.6140216 2.32631032,14.5136785 L2.32631032,5.99227386 C2.32631032,5.89193073 2.3503237,5.83018204 2.39835118,5.80702594 C2.44637866,5.78386983 2.50548852,5.80895523 2.57568253,5.88228291 L6.51022184,9.99247129 C6.53977721,10.0233461 6.56379059,10.0600094 6.5822627,10.1024622 L6.5822627,10.4034901 C6.56379059,10.4420836 6.53977721,10.4787469 6.51022184,10.5134811 L2.57568253,14.6236695 Z" transform="translate(4.454287, 10.252976) scale(1, -1) rotate(90.000000) translate(-4.454287, -10.252976) "></path></svg>
......@@ -10,8 +10,8 @@ class DiscussionModel {
this.canResolve = false;
}
createNote (noteId, canResolve, resolved, resolved_by) {
Vue.set(this.notes, noteId, new NoteModel(this.id, noteId, canResolve, resolved, resolved_by));
createNote (noteObj) {
Vue.set(this.notes, noteObj.noteId, new NoteModel(this.id, noteObj));
}
deleteNote (noteId) {
......
/* eslint-disable camelcase, no-unused-vars */
class NoteModel {
constructor(discussionId, noteId, canResolve, resolved, resolved_by) {
constructor(discussionId, noteObj) {
this.discussionId = discussionId;
this.id = noteId;
this.canResolve = canResolve;
this.resolved = resolved;
this.resolved_by = resolved_by;
this.id = noteObj.noteId;
this.canResolve = noteObj.canResolve;
this.resolved = noteObj.resolved;
this.resolved_by = noteObj.resolvedBy;
this.authorName = noteObj.authorName;
this.authorAvatar = noteObj.authorAvatar;
this.noteTruncated = noteObj.noteTruncated;
}
}
......
......@@ -21,10 +21,10 @@
return discussion;
},
create: function (discussionId, noteId, canResolve, resolved, resolved_by) {
const discussion = this.createDiscussion(discussionId);
create: function (noteObj) {
const discussion = this.createDiscussion(noteObj.discussionId);
discussion.createNote(noteId, canResolve, resolved, resolved_by);
discussion.createNote(noteObj);
},
update: function (discussionId, noteId, resolved, resolved_by) {
const discussion = this.state[discussionId];
......
......@@ -5,7 +5,6 @@ import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make
/* global ShortcutsNavigation */
/* global Build */
/* global Issuable */
/* global Issue */
/* global ShortcutsIssuable */
/* global ZenMode */
/* global Milestone */
......@@ -37,6 +36,7 @@ import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make
/* global Shortcuts */
/* global WeightSelect */
/* global AdminEmailSelect */
import Issue from './issue';
import BindInOut from './behaviors/bind_in_out';
import GroupsList from './groups_list';
......
......@@ -38,6 +38,9 @@
FilesCommentButton.prototype.render = function(e) {
var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button;
$currentTarget = $(e.currentTarget);
if ($currentTarget.hasClass('js-no-comment-btn')) return;
lineContentElement = this.getLineContent($currentTarget);
buttonParentElement = this.getButtonParent($currentTarget);
......
......@@ -80,11 +80,18 @@
}
// Determines the full search query (visual tokens + input)
static getSearchQuery() {
const tokensContainer = document.querySelector('.tokens-container');
static getSearchQuery(untilInput = false) {
const tokens = [].slice.call(document.querySelectorAll('.tokens-container li'));
const values = [];
[].forEach.call(tokensContainer.querySelectorAll('.js-visual-token'), (token) => {
if (untilInput) {
const inputIndex = _.findIndex(tokens, t => t.classList.contains('input-token'));
// Add one to include input-token to the tokens array
tokens.splice(inputIndex + 1);
}
tokens.forEach((token) => {
if (token.classList.contains('js-visual-token')) {
const name = token.querySelector('.name');
const value = token.querySelector('.value');
const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
......@@ -99,10 +106,21 @@
} else {
values.push(name.innerText);
}
});
} else if (token.classList.contains('input-token')) {
const { isLastVisualTokenValid } =
gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const input = document.querySelector('.filtered-search');
values.push(input && input.value);
const inputValue = input && input.value;
if (isLastVisualTokenValid) {
values.push(inputValue);
} else {
const previous = values.pop();
values.push(`${previous}${inputValue}`);
}
}
});
return values.join(' ');
}
......
......@@ -151,7 +151,7 @@
}
setDropdown() {
const query = gl.DropdownUtils.getSearchQuery();
const query = gl.DropdownUtils.getSearchQuery(true);
const { lastToken, searchToken } = this.tokenizer.processTokens(query);
if (this.currentDropdown) {
......
......@@ -178,7 +178,6 @@
if (e.keyCode === 8 || e.keyCode === 46) {
gl.FilteredSearchVisualTokens.removeSelectedToken();
this.handleInputPlaceholder();
this.toggleClearSearchButton();
}
}
......@@ -368,12 +367,15 @@
tokenChange() {
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
if (dropdown) {
const currentDropdownRef = dropdown.reference;
this.setDropdownWrapper();
currentDropdownRef.dispatchInputEvent();
}
}
}
window.gl = window.gl || {};
gl.FilteredSearchManager = FilteredSearchManager;
......
......@@ -5,12 +5,8 @@ require('./flash');
require('vendor/jquery.waitforimages');
require('./task_list');
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.Issue = (function() {
function Issue() {
this.submitNoteForm = bind(this.submitNoteForm, this);
class Issue {
constructor() {
if ($('a.btn-close').length) {
this.taskList = new gl.TaskList({
dataType: 'issue',
......@@ -21,16 +17,15 @@ require('./task_list');
document.querySelector('#task_status_short').innerText = result.task_status_short;
}
});
this.initIssueBtnEventListeners();
Issue.initIssueBtnEventListeners();
}
this.initMergeRequests();
this.initRelatedBranches();
this.initCanCreateBranch();
Issue.initMergeRequests();
Issue.initRelatedBranches();
Issue.initCanCreateBranch();
}
Issue.prototype.initIssueBtnEventListeners = function() {
var _this, issueFailMessage;
_this = this;
static initIssueBtnEventListeners() {
var issueFailMessage;
issueFailMessage = 'Unable to update this issue at this time.';
return $('a.btn-close, a.btn-reopen').on('click', function(e) {
var $this, isClose, shouldSubmit, url;
......@@ -40,7 +35,7 @@ require('./task_list');
isClose = $this.hasClass('btn-close');
shouldSubmit = $this.hasClass('btn-comment');
if (shouldSubmit) {
_this.submitNoteForm($this.closest('form'));
Issue.submitNoteForm($this.closest('form'));
}
$this.prop('disabled', true);
url = $this.attr('href');
......@@ -76,17 +71,17 @@ require('./task_list');
}
});
});
};
}
Issue.prototype.submitNoteForm = function(form) {
static submitNoteForm(form) {
var noteText;
noteText = form.find("textarea.js-note-text").val();
if (noteText.trim().length > 0) {
return form.submit();
}
};
}
Issue.prototype.initMergeRequests = function() {
static initMergeRequests() {
var $container;
$container = $('#merge-requests');
return $.getJSON($container.data('url')).error(function() {
......@@ -96,9 +91,9 @@ require('./task_list');
return $container.html(data.html);
}
});
};
}
Issue.prototype.initRelatedBranches = function() {
static initRelatedBranches() {
var $container;
$container = $('#related-branches');
return $.getJSON($container.data('url')).error(function() {
......@@ -108,9 +103,9 @@ require('./task_list');
return $container.html(data.html);
}
});
};
}
Issue.prototype.initCanCreateBranch = function() {
static initCanCreateBranch() {
var $container;
$container = $('#new-branch');
// If the user doesn't have the required permissions the container isn't
......@@ -128,8 +123,7 @@ require('./task_list');
return $container.find('.unavailable').show();
}
});
};
}
}
return Issue;
})();
}).call(window);
export default Issue;
......@@ -350,11 +350,11 @@ require('./weight_select');
var notesHolders = $this.closest('.diff-file').find('.notes_holder');
$this.toggleClass('active');
if ($this.hasClass('active')) {
notesHolders.show().find('.hide').show();
notesHolders.show().find('.hide, .content').show();
} else {
notesHolders.hide();
notesHolders.hide().find('.content').hide();
}
$this.trigger('blur');
$(document).trigger('toggle.comments');
return e.preventDefault();
});
$document.off('click', '.js-confirm-danger');
......
......@@ -312,7 +312,7 @@ require('./task_list');
*/
Notes.prototype.renderDiscussionNote = function(note) {
var discussionContainer, form, note_html, row;
var discussionContainer, form, note_html, row, lineType, diffAvatarContainer;
if (!this.isNewNote(note)) {
return;
}
......@@ -322,6 +322,8 @@ require('./task_list');
form = $("#new-discussion-note-form-" + note.original_discussion_id);
}
row = form.closest("tr");
lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
note_html = $(note.html);
note_html.renderGFM();
// is this the first note of discussion?
......@@ -330,10 +332,26 @@ require('./task_list');
discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']");
}
if (discussionContainer.length === 0) {
if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) {
// insert the note and the reply button after the temp row
row.after(note.diff_discussion_html);
// remove the note (will be added again below)
row.next().find(".note").remove();
} else {
// Merge new discussion HTML in
var $discussion = $(note.diff_discussion_html);
var $notes = $discussion.find('.notes[data-discussion-id="' + note.discussion_id + '"]');
var contentContainerClass = '.' + $notes.closest('.notes_content')
.attr('class')
.split(' ')
.join('.');
// remove the note (will be added again below)
$notes.find('.note').remove();
row.find(contentContainerClass + ' .content').append($notes.closest('.content').children());
}
// Before that, the container didn't exist
discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
// Add note to 'Changes' page discussions
......@@ -347,14 +365,40 @@ require('./task_list');
discussionContainer.append(note_html);
}
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
if (typeof gl.diffNotesCompileComponents !== 'undefined' && note.discussion_id) {
gl.diffNotesCompileComponents();
this.renderDiscussionAvatar(diffAvatarContainer, note);
}
gl.utils.localTimeAgo($('.js-timeago'), false);
return this.updateNotesCount(1);
};
Notes.prototype.getLineHolder = function(changesDiscussionContainer) {
return $(changesDiscussionContainer).closest('.notes_holder')
.prevAll('.line_holder')
.first()
.get(0);
};
Notes.prototype.renderDiscussionAvatar = function(diffAvatarContainer, note) {
var commentButton = diffAvatarContainer.find('.js-add-diff-note-button');
var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders');
if (!avatarHolder.length) {
avatarHolder = document.createElement('diff-note-avatars');
avatarHolder.setAttribute('discussion-id', note.discussion_id);
diffAvatarContainer.append(avatarHolder);
gl.diffNotesCompileComponents();
}
if (commentButton.length) {
commentButton.remove();
}
};
/*
Called in response the main target form has been successfully submitted.
......@@ -592,9 +636,14 @@ require('./task_list');
*/
Notes.prototype.removeNote = function(e) {
var noteId;
noteId = $(e.currentTarget).closest(".note").attr("id");
$(".note[id='" + noteId + "']").each((function(_this) {
var noteElId, noteId, dataNoteId, $note, lineHolder;
$note = $(e.currentTarget).closest('.note');
noteElId = $note.attr('id');
noteId = $note.attr('data-note-id');
lineHolder = $(e.currentTarget).closest('.notes[data-discussion-id]')
.closest('.notes_holder')
.prev('.line_holder');
$(".note[id='" + noteElId + "']").each((function(_this) {
// A same note appears in the "Discussion" and in the "Changes" tab, we have
// to remove all. Using $(".note[id='noteId']") ensure we get all the notes,
// where $("#noteId") would return only one.
......@@ -604,17 +653,26 @@ require('./task_list');
notes = note.closest(".notes");
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
if (gl.diffNoteApps[noteId]) {
gl.diffNoteApps[noteId].$destroy();
if (gl.diffNoteApps[noteElId]) {
gl.diffNoteApps[noteElId].$destroy();
}
}
note.remove();
// check if this is the last note for this line
if (notes.find(".note").length === 1) {
if (notes.find(".note").length === 0) {
var notesTr = notes.closest("tr");
// "Discussions" tab
notes.closest(".timeline-entry").remove();
if (!_this.isParallelView() || notesTr.find('.note').length === 0) {
// "Changes" tab / commit view
notes.closest("tr").remove();
notesTr.remove();
} else {
notes.closest('.content').empty();
}
}
return note.remove();
};
......@@ -707,15 +765,16 @@ require('./task_list');
*/
Notes.prototype.addDiffNote = function(e) {
var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent;
var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar;
e.preventDefault();
$link = $(e.currentTarget);
$link = $(e.currentTarget || e.target);
row = $link.closest("tr");
nextRow = row.next();
hasNotes = nextRow.is(".notes_holder");
addForm = false;
notesContentSelector = ".notes_content";
rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"><div class=\"content\"></div></td></tr>";
isDiffCommentAvatar = $link.hasClass('js-diff-comment-avatar');
// In parallel view, look inside the correct left/right pane
if (this.isParallelView()) {
lineType = $link.data("lineType");
......@@ -723,7 +782,9 @@ require('./task_list');
rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line old\"></td><td class=\"notes_content parallel old\"><div class=\"content\"></div></td><td class=\"notes_line new\"></td><td class=\"notes_content parallel new\"><div class=\"content\"></div></td></tr>";
}
notesContentSelector += " .content";
if (hasNotes) {
notesContent = nextRow.find(notesContentSelector);
if (hasNotes && !isDiffCommentAvatar) {
nextRow.show();
notesContent = nextRow.find(notesContentSelector);
if (notesContent.length) {
......@@ -740,13 +801,21 @@ require('./task_list');
}
}
}
} else {
} else if (!isDiffCommentAvatar) {
// add a notes row and insert the form
row.after(rowCssToAdd);
nextRow = row.next();
notesContent = nextRow.find(notesContentSelector);
addForm = true;
} else {
nextRow.show();
notesContent.toggle(!notesContent.is(':visible'));
if (!nextRow.find('.content:not(:empty)').is(':visible')) {
nextRow.hide();
}
}
if (addForm) {
newForm = this.formClone.clone();
newForm.appendTo(notesContent);
......
......@@ -4,6 +4,21 @@
&.reset-filters {
padding: 7px;
}
&.update-issues-btn {
float: right;
margin-right: 0;
@media (max-width: $screen-xs-max) {
float: none;
}
}
}
.filters-section {
@media (max-width: $screen-xs-max) {
display: inline-block;
}
}
@media (min-width: $screen-sm-min) {
......@@ -34,6 +49,11 @@
display: block;
margin: 0 0 10px;
}
.dropdown-menu-toggle,
.update-issues-btn .btn {
width: 100%;
}
}
.filtered-search-container {
......@@ -208,7 +228,15 @@
overflow: auto;
}
@media (max-width: $screen-xs-min) {
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
.issues-details-filters {
.dropdown-menu-toggle {
width: 100px;
}
}
}
@media (max-width: $screen-xs-max) {
.issues-details-filters {
padding: 0 0 10px;
background-color: $white-light;
......
......@@ -235,44 +235,6 @@ ul.content-list {
}
}
// Table list
.table-list {
display: table;
width: 100%;
.table-list-row {
display: table-row;
}
.table-list-cell {
display: table-cell;
vertical-align: top;
padding: 10px 16px;
border-bottom: 1px solid $gray-darker;
&.avatar-cell {
width: 36px;
padding-right: 0;
img {
margin-right: 0;
}
}
}
&.table-wide {
.table-list-cell {
&:last-of-type {
padding-right: 0;
}
&:first-of-type {
padding-left: 0;
}
}
}
}
.panel > .content-list > li {
padding: $gl-padding-top $gl-padding;
}
......
......@@ -100,8 +100,7 @@
@media (max-width: $screen-sm-max) {
.issues-filters {
.milestone-filter,
.labels-filter {
.milestone-filter {
display: none;
}
}
......
......@@ -294,7 +294,7 @@
.nav-control {
@media (max-width: $screen-sm-max) {
margin-right: 75px;
margin-right: 2px;
}
}
}
......
......@@ -48,11 +48,3 @@
line-height: inherit;
}
}
.panel-default {
.table-list-row:last-child {
.table-list-cell {
border-bottom: 0;
}
}
}
......@@ -78,6 +78,7 @@
padding: 5px 10px;
background-color: $gray-light;
border-bottom: 1px solid $gray-darker;
border-top: 1px solid $gray-darker;
font-size: 14px;
&:first-child {
......@@ -117,10 +118,37 @@
}
}
.commit.flex-list {
display: flex;
}
.avatar-cell {
width: 46px;
padding-left: 10px;
img {
margin-right: 0;
}
}
.commit-detail {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-grow: 1;
padding-left: 10px;
.merge-request-branches & {
flex-direction: column;
}
}
.commit-content {
padding-right: 10px;
}
.commit-actions {
@media (min-width: $screen-sm-min) {
width: 300px;
text-align: right;
font-size: 0;
}
......
......@@ -113,6 +113,10 @@
td.line_content.parallel {
width: 46%;
}
.add-diff-note {
margin-left: -55px;
}
}
.old_line,
......@@ -490,3 +494,103 @@
}
}
}
.diff-comment-avatar-holders {
position: absolute;
height: 19px;
width: 19px;
margin-left: -15px;
&:hover {
.diff-comment-avatar,
.diff-comments-more-count {
@for $i from 1 through 4 {
$x-pos: 14px;
&:nth-child(#{$i}) {
@if $i == 4 {
$x-pos: 14.5px;
}
transform: translateX((($i * $x-pos) - $x-pos));
&:hover {
transform: translateX((($i * $x-pos) - $x-pos)) scale(1.2);
}
}
}
}
.diff-comments-more-count {
padding-left: 2px;
padding-right: 2px;
width: auto;
}
}
}
.diff-comment-avatar,
.diff-comments-more-count {
position: absolute;
left: 0;
width: 19px;
height: 19px;
margin-right: 0;
border-color: $white-light;
cursor: pointer;
transition: all .1s ease-out;
@for $i from 1 through 4 {
&:nth-child(#{$i}) {
z-index: (4 - $i);
}
}
}
.diff-comments-more-count {
width: 19px;
min-width: 19px;
padding-left: 0;
padding-right: 0;
overflow: hidden;
}
.diff-comments-more-count,
.diff-notes-collapse {
background-color: $gray-darkest;
color: $white-light;
border: 1px solid $white-light;
border-radius: 1em;
font-family: $regular_font;
font-size: 9px;
line-height: 17px;
text-align: center;
}
.diff-notes-collapse {
position: relative;
width: 19px;
height: 19px;
padding: 0;
transition: transform .1s ease-out;
svg {
position: absolute;
left: 50%;
top: 50%;
margin-left: -5.5px;
margin-top: -5.5px;
}
path {
fill: $white-light;
}
&:hover {
transform: scale(1.2);
}
&:focus {
outline: 0;
}
}
......@@ -240,8 +240,7 @@
.commit {
margin: 0;
padding-top: 2px;
padding-bottom: 2px;
padding: 10px 0;
list-style: none;
&:hover {
......@@ -409,7 +408,7 @@
}
.panel-footer {
padding: 5px 10px;
padding: 0;
.btn {
min-width: auto;
......
......@@ -331,6 +331,10 @@ ul.notes {
&:hover {
color: $gl-link-color;
}
&:focus,
&:hover {
text-decoration: none;
}
}
......
......@@ -115,7 +115,7 @@
.table.ci-table {
&.builds-page tr {
&.builds-page tbody tr {
height: 71px;
}
......
......@@ -759,6 +759,8 @@ a.allowed-to-push {
}
.protected-branches-list {
margin-bottom: 30px;
a {
color: $gl-text-color;
......
......@@ -24,3 +24,14 @@
.service-settings .control-label {
padding-top: 0;
}
.token-token-container {
#impersonation-token-token {
width: 80%;
display: inline;
}
.btn-clipboard {
margin-left: 5px;
}
}
.triggers-container {
.label-container {
display: inline-block;
margin-left: 10px;
}
}
.trigger-actions {
.btn {
margin-left: 10px;
}
}
......@@ -139,18 +139,10 @@
.blob-commit-info {
list-style: none;
background: $gray-light;
padding: 6px 0;
padding: 16px 16px 16px 6px;
border: 1px solid $border-color;
border-bottom: none;
margin: 0;
.table-list-cell {
border-bottom: none;
}
.commit-actions {
width: 260px;
}
}
#modal-remove-blob > .modal-dialog { width: 850px; }
......
......@@ -2,7 +2,7 @@ class Admin::ApplicationsController < Admin::ApplicationController
include OauthApplications
before_action :set_application, only: [:show, :edit, :update, :destroy]
before_action :load_scopes, only: [:new, :edit]
before_action :load_scopes, only: [:new, :create, :edit, :update]
def index
@applications = Doorkeeper::Application.where("owner_id IS NULL")
......
class Admin::ImpersonationTokensController < Admin::ApplicationController
before_action :user
def index
set_index_vars
end
def create
@impersonation_token = finder.build(impersonation_token_params)
if @impersonation_token.save
flash[:impersonation_token] = @impersonation_token.token
redirect_to admin_user_impersonation_tokens_path, notice: "A new impersonation token has been created."
else
set_index_vars
render :index
end
end
def revoke
@impersonation_token = finder.find(params[:id])
if @impersonation_token.revoke!
flash[:notice] = "Revoked impersonation token #{@impersonation_token.name}!"
else
flash[:alert] = "Could not revoke impersonation token #{@impersonation_token.name}."
end
redirect_to admin_user_impersonation_tokens_path
end
private
def user
@user ||= User.find_by!(username: params[:user_id])
end
def finder(options = {})
PersonalAccessTokensFinder.new({ user: user, impersonation: true }.merge(options))
end
def impersonation_token_params
params.require(:personal_access_token).permit(:name, :expires_at, :impersonation, scopes: [])
end
def set_index_vars
@scopes = Gitlab::Auth::API_SCOPES
@impersonation_token ||= finder.build
@inactive_impersonation_tokens = finder(state: 'inactive').execute
@active_impersonation_tokens = finder(state: 'active').execute.order(:expires_at)
end
end
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
before_action :authenticate_resource_owner!
layout 'profile'
# Overriden from Doorkeeper::AuthorizationsController to
# include the call to session.delete
def new
if pre_auth.authorizable?
if skip_authorization? || matching_token?
......@@ -16,44 +16,4 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
render "doorkeeper/authorizations/error"
end
end
# TODO: Handle raise invalid authorization
def create
redirect_or_render authorization.authorize
end
def destroy
redirect_or_render authorization.deny
end
private
def matching_token?
Doorkeeper::AccessToken.matching_token_for(pre_auth.client,
current_resource_owner.id,
pre_auth.scopes)
end
def redirect_or_render(auth)
if auth.redirectable?
redirect_to auth.redirect_uri
else
render json: auth.body, status: auth.status
end
end
def pre_auth
@pre_auth ||=
Doorkeeper::OAuth::PreAuthorization.new(Doorkeeper.configuration,
server.client_via_uid,
params)
end
def authorization
@authorization ||= strategy.request
end
def strategy
@strategy ||= server.authorization_request(pre_auth.response_type)
end
end
......@@ -4,7 +4,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end
def create
@personal_access_token = current_user.personal_access_tokens.generate(personal_access_token_params)
@personal_access_token = finder.build(personal_access_token_params)
if @personal_access_token.save
flash[:personal_access_token] = @personal_access_token.token
......@@ -16,7 +16,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end
def revoke
@personal_access_token = current_user.personal_access_tokens.find(params[:id])
@personal_access_token = finder.find(params[:id])
if @personal_access_token.revoke!
flash[:notice] = "Revoked personal access token #{@personal_access_token.name}!"
......@@ -29,14 +29,19 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
private
def finder(options = {})
PersonalAccessTokensFinder.new({ user: current_user, impersonation: false }.merge(options))
end
def personal_access_token_params
params.require(:personal_access_token).permit(:name, :expires_at, scopes: [])
end
def set_index_vars
@personal_access_token ||= current_user.personal_access_tokens.build
@scopes = Gitlab::Auth::SCOPES
@active_personal_access_tokens = current_user.personal_access_tokens.active.order(:expires_at)
@inactive_personal_access_tokens = current_user.personal_access_tokens.inactive
@scopes = Gitlab::Auth::API_SCOPES
@personal_access_token = finder.build
@inactive_personal_access_tokens = finder(state: 'inactive').execute
@active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at)
end
end
......@@ -35,10 +35,14 @@ module Projects
def access_levels_options
{
push_access_levels: {
roles: ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } },
roles: ProtectedBranch::PushAccessLevel.human_access_levels.map do |id, text|
{ id: id, text: text, before_divider: true }
end
},
merge_access_levels: {
roles: ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } },
roles: ProtectedBranch::MergeAccessLevel.human_access_levels.map do |id, text|
{ id: id, text: text, before_divider: true }
end
},
selected_merge_access_levels: @protected_branch.merge_access_levels.map { |access_level| access_level.user_id || access_level.access_level },
selected_push_access_levels: @protected_branch.push_access_levels.map { |access_level| access_level.user_id || access_level.access_level }
......
class Projects::TriggersController < Projects::ApplicationController
before_action :authorize_admin_build!
before_action :authorize_manage_trigger!, except: [:index, :create]
before_action :authorize_admin_trigger!, only: [:edit, :update]
before_action :trigger, only: [:take_ownership, :edit, :update, :destroy]
layout 'project_settings'
......@@ -8,27 +11,67 @@ class Projects::TriggersController < Projects::ApplicationController
end
def create
@trigger = project.triggers.new
@trigger.save
@trigger = project.triggers.create(create_params.merge(owner: current_user))
if @trigger.valid?
redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Trigger was created successfully.'
flash[:notice] = 'Trigger was created successfully.'
else
@triggers = project.triggers.select(&:persisted?)
render action: "show"
flash[:alert] = 'You could not create a new trigger.'
end
redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
def take_ownership
if trigger.update(owner: current_user)
flash[:notice] = 'Trigger was re-assigned.'
else
flash[:alert] = 'You could not take ownership of trigger.'
end
redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
def edit
end
def update
if trigger.update(update_params)
redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project), notice: 'Trigger was successfully updated.'
else
render action: "edit"
end
end
def destroy
trigger.destroy
flash[:alert] = "Trigger removed"
if trigger.destroy
flash[:notice] = "Trigger removed."
else
flash[:alert] = "Could not remove the trigger."
end
redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end
private
def authorize_manage_trigger!
access_denied! unless can?(current_user, :manage_trigger, trigger)
end
def authorize_admin_trigger!
access_denied! unless can?(current_user, :admin_trigger, trigger)
end
def trigger
@trigger ||= project.triggers.find(params[:id])
@trigger ||= project.triggers.find(params[:id]) || render_404
end
def create_params
params.require(:trigger).permit(:description)
end
def update_params
params.require(:trigger).permit(:description)
end
end
......@@ -14,6 +14,8 @@ class UploadsController < ApplicationController
end
disposition = uploader.image? ? 'inline' : 'attachment'
expires_in 0.seconds, must_revalidate: true, private: true
send_file uploader.file.path, disposition: disposition
end
......
class PersonalAccessTokensFinder
attr_accessor :params
delegate :build, :find, :find_by, to: :execute
def initialize(params = {})
@params = params
end
def execute
tokens = PersonalAccessToken.all
tokens = by_user(tokens)
tokens = by_impersonation(tokens)
by_state(tokens)
end
private
def by_user(tokens)
return tokens unless @params[:user]
tokens.where(user: @params[:user])
end
def by_impersonation(tokens)
case @params[:impersonation]
when true
tokens.with_impersonation
when false
tokens.without_impersonation
else
tokens
end
end
def by_state(tokens)
case @params[:state]
when 'active'
tokens.active
when 'inactive'
tokens.inactive
else
tokens
end
end
end
......@@ -12,7 +12,7 @@ module BuildsHelper
build_url: namespace_project_build_url(@project.namespace, @project, @build, :json),
build_status: @build.status,
build_stage: @build.stage,
log_state: @build.trace_with_state[:state].to_s
log_state: ''
}
end
......
......@@ -15,6 +15,8 @@ module CiStatusHelper
'passed'
when 'success_with_warnings'
'passed with warnings'
when 'manual'
'waiting for manual action'
else
status
end
......@@ -48,6 +50,8 @@ module CiStatusHelper
'icon_status_created'
when 'skipped'
'icon_status_skipped'
when 'manual'
'icon_status_manual'
else
'icon_status_canceled'
end
......
......@@ -162,7 +162,12 @@ module EventsHelper
def event_note(text, options = {})
text = first_line_in_markdown(text, 150, options)
sanitize(text, tags: %w(a img b pre code p span))
sanitize(
text,
tags: %w(a img b pre code p span),
attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style']
)
end
def event_commit_title(message)
......
......@@ -36,7 +36,7 @@ module PreferencesHelper
def project_view_choices
[
['Files and Readme (default)', :files],
['Activity view', :activity]
['Activity', :activity]
]
end
......
......@@ -52,7 +52,7 @@ module SortingHelper
end
def sort_title_priority
'Priority'
'Label priority'
end
def sort_title_oldest_updated
......
class ChatTeam < ActiveRecord::Base
validates :team_id, presence: true
validates :namespace, uniqueness: true
belongs_to :namespace
end
......@@ -518,6 +518,27 @@ module Ci
]
end
def steps
[Gitlab::Ci::Build::Step.from_commands(self),
Gitlab::Ci::Build::Step.from_after_script(self)].compact
end
def image
Gitlab::Ci::Build::Image.from_image(self)
end
def services
Gitlab::Ci::Build::Image.from_services(self)
end
def artifacts
[options[:artifacts]]
end
def cache
[options[:cache]]
end
def credentials
Gitlab::Ci::Build::Credentials::Factory.new(self).create!
end
......@@ -544,10 +565,35 @@ module Ci
@unscoped_project ||= Project.unscoped.find_by(id: gl_project_id)
end
CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
def predefined_variables
variables = [
{ key: 'CI', value: 'true', public: true },
{ key: 'GITLAB_CI', value: 'true', public: true },
{ key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
{ key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
{ key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
{ key: 'CI_JOB_ID', value: id.to_s, public: true },
{ key: 'CI_JOB_NAME', value: name, public: true },
{ key: 'CI_JOB_STAGE', value: stage, public: true },
{ key: 'CI_JOB_TOKEN', value: token, public: false },
{ key: 'CI_COMMIT_SHA', value: sha, public: true },
{ key: 'CI_COMMIT_REF_NAME', value: ref, public: true },
{ key: 'CI_COMMIT_REF_SLUG', value: ref_slug, public: true },
{ key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER, public: true },
{ key: 'CI_REGISTRY_PASSWORD', value: token, public: false },
{ key: 'CI_REPOSITORY_URL', value: repo_url, public: false }
]
variables << { key: "CI_COMMIT_TAG", value: ref, public: true } if tag?
variables << { key: "CI_PIPELINE_TRIGGERED", value: 'true', public: true } if trigger_request
variables << { key: "CI_JOB_MANUAL", value: 'true', public: true } if action?
variables.concat(legacy_variables)
end
def legacy_variables
variables = [
{ key: 'CI_BUILD_ID', value: id.to_s, public: true },
{ key: 'CI_BUILD_TOKEN', value: token, public: false },
{ key: 'CI_BUILD_REF', value: sha, public: true },
......@@ -555,14 +601,12 @@ module Ci
{ key: 'CI_BUILD_REF_NAME', value: ref, public: true },
{ key: 'CI_BUILD_REF_SLUG', value: ref_slug, public: true },
{ key: 'CI_BUILD_NAME', value: name, public: true },
{ key: 'CI_BUILD_STAGE', value: stage, public: true },
{ key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
{ key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
{ key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }
{ key: 'CI_BUILD_STAGE', value: stage, public: true }
]
variables << { key: 'CI_BUILD_TAG', value: ref, public: true } if tag?
variables << { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } if trigger_request
variables << { key: 'CI_BUILD_MANUAL', value: 'true', public: true } if action?
variables << { key: "CI_BUILD_TAG", value: ref, public: true } if tag?
variables << { key: "CI_BUILD_TRIGGERED", value: 'true', public: true } if trigger_request
variables << { key: "CI_BUILD_MANUAL", value: 'true', public: true } if action?
variables
end
......
......@@ -29,8 +29,12 @@ module Ci
token[0...4]
end
def can_show_token?(user)
owner.blank? || owner == user
def legacy?
self.owner_id.blank?
end
def can_access_project?
self.owner_id.blank? || Ability.allowed?(self.owner, :create_build, project)
end
end
end
......@@ -7,7 +7,7 @@ module HasStatus
STARTED_STATUSES = %w[running success failed skipped manual].freeze
ACTIVE_STATUSES = %w[pending running].freeze
COMPLETED_STATUSES = %w[success failed canceled skipped].freeze
ORDERED_STATUSES = %w[manual failed pending running canceled success skipped].freeze
ORDERED_STATUSES = %w[failed pending running manual canceled success skipped created].freeze
class_methods do
def status_sql
......
class Geo::BaseRegistry < ActiveRecord::Base
self.abstract_class = true
if Gitlab::Geo.secondary? || Rails.env.test?
if Gitlab::Geo.secondary? || (Rails.env.test? && Rails.configuration.respond_to?(:geo_database))
establish_connection Rails.configuration.geo_database
end
end
class OauthAccessGrant < Doorkeeper::AccessGrant
belongs_to :resource_owner, class_name: 'User'
belongs_to :application, class_name: 'Doorkeeper::Application'
end
class OauthAccessToken < ActiveRecord::Base
class OauthAccessToken < Doorkeeper::AccessToken
belongs_to :resource_owner, class_name: 'User'
belongs_to :application, class_name: 'Doorkeeper::Application'
end
class PersonalAccessToken < ActiveRecord::Base
include Expirable
include TokenAuthenticatable
add_authentication_token_field :token
......@@ -6,17 +7,30 @@ class PersonalAccessToken < ActiveRecord::Base
belongs_to :user
scope :active, -> { where(revoked: false).where("expires_at >= NOW() OR expires_at IS NULL") }
before_save :ensure_token
scope :active, -> { where("revoked = false AND (expires_at >= NOW() OR expires_at IS NULL)") }
scope :inactive, -> { where("revoked = true OR expires_at < NOW()") }
scope :with_impersonation, -> { where(impersonation: true) }
scope :without_impersonation, -> { where(impersonation: false) }
def self.generate(params)
personal_access_token = self.new(params)
personal_access_token.ensure_token
personal_access_token
end
validates :scopes, presence: true
validate :validate_api_scopes
def revoke!
self.revoked = true
self.save
end
def active?
!revoked? && !expired?
end
protected
def validate_api_scopes
unless scopes.all? { |scope| Gitlab::Auth::API_SCOPES.include?(scope.to_sym) }
errors.add :scopes, "can only contain API scopes"
end
end
end
......@@ -36,7 +36,7 @@ class KubernetesService < DeploymentService
def initialize_properties
if properties.nil?
self.properties = {}
self.namespace = project.path if project.present?
self.namespace = "#{project.path}-#{project.id}" if project.present?
end
end
......
......@@ -320,11 +320,13 @@ class Repository
if !branch_name || branch_name == root_ref
branches.each do |branch|
cache.expire(:"diverging_commit_counts_#{branch.name}")
cache.expire(:"commit_count_#{branch.name}")
end
# In case a commit is pushed to a non-root branch we only have to flush the
# cache for said branch.
else
cache.expire(:"diverging_commit_counts_#{branch_name}")
cache.expire(:"commit_count_#{branch_name}")
end
end
......@@ -504,6 +506,16 @@ class Repository
end
cache_method :commit_count, fallback: 0
def commit_count_for_ref(ref)
return 0 unless exists?
begin
cache.fetch(:"commit_count_#{ref}") { raw_repository.commit_count(ref) }
rescue Rugged::ReferenceError
0
end
end
def branch_names
branches.map(&:name)
end
......
......@@ -345,8 +345,7 @@ class User < ActiveRecord::Base
end
def find_by_personal_access_token(token_string)
personal_access_token = PersonalAccessToken.active.find_by_token(token_string) if token_string
personal_access_token&.user
PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user
end
# Returns a user for the given SSH key.
......
module Ci
class TriggerPolicy < BasePolicy
def rules
delegate! @subject.project
if can?(:admin_build)
can! :admin_trigger if @subject.owner.blank? ||
@subject.owner == @user
can! :manage_trigger
end
end
end
end
module Ci
# This class responsible for assigning
# proper pending build to runner on runner API request
class RegisterBuildService
class RegisterJobService
include Gitlab::CurrentSettings
prepend EE::Ci::RegisterBuildService
prepend EE::Ci::RegisterJobService
attr_reader :runner
......
module EE
module Ci
# RegisterBuildService EE mixin
# RegisterJobService EE mixin
#
# This module is intended to encapsulate EE-specific service logic
# and be included in the `RegisterBuildService` service
module RegisterBuildService
# and be included in the `RegisterJobService` service
module RegisterJobService
extend ActiveSupport::Concern
def builds_for_shared_runner
......
......@@ -104,6 +104,7 @@ class GitPushService < BaseService
.perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref])
mirror_update = @project.mirror? && @project.repository.up_to_date_with_upstream?(branch_name)
SystemHookPushWorker.perform_async(build_push_data.dup, :push_hooks)
EventCreateService.new.push(@project, current_user, build_push_data)
@project.execute_hooks(build_push_data.dup, :push_hooks)
......
......@@ -35,13 +35,22 @@ class NamespaceValidator < ActiveModel::EachValidator
users
].freeze
WILDCARD_ROUTES = %w[tree commits wikis new edit create update logs_tree
preview blob blame raw files create_dir find_file].freeze
STRICT_RESERVED = (RESERVED + WILDCARD_ROUTES).freeze
def self.valid?(value)
!reserved?(value) && follow_format?(value)
end
def self.reserved?(value)
def self.reserved?(value, strict: false)
if strict
STRICT_RESERVED.include?(value)
else
RESERVED.include?(value)
end
end
def self.follow_format?(value)
value =~ Gitlab::Regex.namespace_regex
......@@ -54,7 +63,9 @@ class NamespaceValidator < ActiveModel::EachValidator
record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
end
if reserved?(value)
strict = record.is_a?(Group) && record.parent_id
if reserved?(value, strict: strict)
record.errors.add(attribute, "#{value} is a reserved name")
end
end
......
......@@ -14,10 +14,8 @@ class ProjectPathValidator < ActiveModel::EachValidator
# without tree as reserved name routing can match 'group/project' as group name,
# 'tree' as project name and 'deploy_keys' as route.
#
RESERVED = (NamespaceValidator::RESERVED -
%w[dashboard help ci admin search notes services assets profile public] +
%w[tree commits wikis new edit create update logs_tree
preview blob blame raw files create_dir find_file]).freeze
RESERVED = (NamespaceValidator::STRICT_RESERVED -
%w[dashboard help ci admin search notes services assets profile public]).freeze
def self.valid?(value)
!reserved?(value)
......
- page_title "Impersonation Tokens", @user.name, "Users"
= render 'admin/users/head'
.row.prepend-top-default
.col-lg-12
= render "shared/personal_access_tokens_form", path: admin_user_impersonation_tokens_path, impersonation: true, token: @impersonation_token, scopes: @scopes
= render "shared/personal_access_tokens_table", impersonation: true, active_tokens: @active_impersonation_tokens, inactive_tokens: @inactive_impersonation_tokens
......@@ -23,4 +23,6 @@
= link_to "SSH keys", keys_admin_user_path(@user)
= nav_link(controller: :identities) do
= link_to "Identities", admin_user_identities_path(@user)
= nav_link(controller: :impersonation_tokens) do
= link_to "Impersonation Tokens", admin_user_impersonation_tokens_path(@user)
.append-bottom-default
......@@ -6,10 +6,8 @@
.top-area
= render 'shared/issuable/nav', type: :issues
.nav-controls
= link_to params.merge(rss_url_options), class: 'btn' do
= link_to params.merge(rss_url_options), class: 'btn has-tooltip', title: 'Subscribe' do
= icon('rss')
%span.icon-label
Subscribe
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
= render 'shared/issuable/filter', type: :issues
......
......@@ -6,7 +6,7 @@
- if @last_push
= render "events/event_last_push", event: @last_push
- if @projects.any?
- if @projects.any? || params[:filter_projects]
= render 'projects'
- else
%h3 You don't have starred projects yet
......
- if inject_u2f_api?
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('u2f.js')
= page_specific_javascript_bundle_tag('u2f')
%div
= render 'devise/shared/tab_single', tab_title: 'Two-Factor Authentication'
......
......@@ -2,5 +2,5 @@
%tr.notes_holder{ class: ('hide' unless expanded) }
%td.notes_line{ colspan: 2 }
%td.notes_content
.content
.content{ class: ('hide' unless expanded) }
= render "discussions/notes", discussion: discussion
......@@ -27,6 +27,7 @@
= hidden_field_tag :state, @pre_auth.state
= hidden_field_tag :response_type, @pre_auth.response_type
= hidden_field_tag :scope, @pre_auth.scope
= hidden_field_tag :nonce, @pre_auth.nonce
= submit_tag "Authorize", class: "btn btn-success wide pull-left"
= form_tag oauth_authorization_path, method: :delete do
= hidden_field_tag :client_id, @pre_auth.client.uid
......@@ -34,4 +35,5 @@
= hidden_field_tag :state, @pre_auth.state
= hidden_field_tag :response_type, @pre_auth.response_type
= hidden_field_tag :scope, @pre_auth.scope
= hidden_field_tag :nonce, @pre_auth.nonce
= submit_tag "Deny", class: "btn btn-danger prepend-left-10"
- if current_user
.controls
.dropdown.project-settings-dropdown
%a.dropdown-new.btn.btn-default#project-settings-button{ href: '#', 'data-toggle' => 'dropdown' }
= icon('cog')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- can_edit = can?(current_user, :admin_project, @project)
= render 'layouts/nav/project_settings', can_edit: can_edit
- if can_edit
%li.divider
%li
= link_to edit_project_path(@project) do
Edit Project
- can_edit = can?(current_user, :admin_project, @project)
.scrolling-tabs-container{ class: nav_control_class }
.fade-left
= icon('angle-left')
......@@ -71,6 +55,17 @@
%span
Snippets
- if project_nav_tab? :settings
= nav_link(path: %w[projects#edit members#show integrations#show repository#show ci_cd#show pages#show]) do
= link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do
%span
Settings
- else
= nav_link(path: %w[members#show]) do
= link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Settings', class: 'shortcuts-tree' do
%span
Settings
-# Shortcut to Project > Activity
%li.hidden
= link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do
......
- if project_nav_tab? :team
= nav_link(controller: [:members, :teams]) do
= link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
%span
Members
- if can_edit
= nav_link(controller: :repository) do
= link_to namespace_project_settings_repository_path(@project.namespace, @project), title: 'Repository' do
%span
Repository
= nav_link(controller: :integrations) do
= link_to namespace_project_settings_integrations_path(@project.namespace, @project), title: 'Integrations' do
%span
Integrations
- if @project.feature_available?(:builds, current_user)
= nav_link(controller: :ci_cd) do
= link_to namespace_project_settings_ci_cd_path(@project.namespace, @project), title: 'CI/CD Pipelines' do
%span
CI/CD Pipelines
= nav_link(controller: :pages) do
= link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages', data: {placement: 'right'} do
%span
Pages
= nav_link(controller: :audit_events) do
= link_to namespace_project_audit_events_path(@project.namespace, @project), title: "Audit Events" do
%span
Audit Events
......@@ -24,80 +24,11 @@
%hr
%h5.prepend-top-0
Add a Personal Access Token
%p.profile-settings-content
Pick a name for the application, and we'll give you a unique token.
= render "form", personal_access_token: @personal_access_token, scopes: @scopes
%hr
%h5 Active Personal Access Tokens (#{@active_personal_access_tokens.length})
- if @active_personal_access_tokens.present?
.table-responsive
%table.table.active-personal-access-tokens
%thead
%tr
%th Name
%th Created
%th Expires
%th Scopes
%th
%tbody
- @active_personal_access_tokens.each do |token|
%tr
%td= token.name
%td= token.created_at.to_date.to_s(:medium)
%td
- if token.expires_at.present?
= token.expires_at.to_date.to_s(:medium)
- else
%span.personal-access-tokens-never-expires-label Never
%td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>"
%td= link_to "Revoke", revoke_profile_personal_access_token_path(token), method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this token? This action cannot be undone." }
- else
.settings-message.text-center
You don't have any active tokens yet.
%hr
%h5 Inactive Personal Access Tokens (#{@inactive_personal_access_tokens.length})
- if @inactive_personal_access_tokens.present?
.table-responsive
%table.table.inactive-personal-access-tokens
%thead
%tr
%th Name
%th Created
%tbody
- @inactive_personal_access_tokens.each do |token|
%tr
%td= token.name
%td= token.created_at.to_date.to_s(:medium)
- else
.settings-message.text-center
There are no inactive tokens.
= render "shared/personal_access_tokens_form", path: profile_personal_access_tokens_path, impersonation: false, token: @personal_access_token, scopes: @scopes
= render "shared/personal_access_tokens_table", impersonation: false, active_tokens: @active_personal_access_tokens, inactive_tokens: @inactive_personal_access_tokens
:javascript
var $dateField = $('#personal_access_token_expires_at');
var date = $dateField.val();
new Pikaday({
field: $dateField.get(0),
theme: 'gitlab-theme',
format: 'yyyy-mm-dd',
minDate: new Date(),
onSelect: function(dateText) {
$dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
});
$("#created-personal-access-token").click(function() {
this.select();
});
......@@ -4,7 +4,7 @@
- if inject_u2f_api?
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('u2f.js')
= page_specific_javascript_bundle_tag('u2f')
.row.prepend-top-default
.col-lg-3
......@@ -96,4 +96,3 @@
:javascript
var button = "<a class='btn btn-xs btn-warning pull-right' data-method='patch' href='#{skip_profile_two_factor_auth_path}'>Configure it later</a>";
$(".flash-alert").append(button);
- @no_container = true
= render "projects/head"
%div{ class: container_class }
.nav-block.activity-filter-block
......
- page_title "Activity"
= render "projects/head"
= render 'projects/last_push'
......
......@@ -19,7 +19,7 @@
= link_to title, '#'
= render_lock_icon(part_path)
%ul.blob-commit-info.table-list.hidden-xs
%ul.blob-commit-info.hidden-xs
- blob_commit = @repository.last_commit_for_path(@commit.id, blob.path)
= render blob_commit, project: @project, ref: @ref
......
- @no_container = true
- page_title "#{@build.name} (##{@build.id})", "Jobs"
- trace_with_state = @build.trace_with_state
= render "projects/pipelines/head", build_subnav: true
%div{ class: container_class }
......
......@@ -34,6 +34,7 @@
= revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false)
%li.clearfix
= cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false)
- if can_collaborate_with_project?
%li.clearfix
= link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit)
%li.divider
......
......@@ -9,12 +9,13 @@
- cache_key.push(commit.status(ref)) if commit.status(ref)
= cache(cache_key, expires_in: 1.day) do
%li.commit.table-list-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
%li.commit.flex-list.js-toggle-container{ id: "commit-#{commit.short_id}" }
.table-list-cell.avatar-cell.hidden-xs
.avatar-cell.hidden-xs
= author_avatar(commit, size: 36)
.table-list-cell.commit-content
.commit-detail
.commit-content
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message item-title"
%span.commit-row-message.visible-xs-inline
&middot;
......@@ -33,7 +34,7 @@
committed
#{time_ago_with_tooltip(commit.committed_date)}
.table-list-cell.commit-actions.hidden-xs
.commit-actions.flex-row.hidden-xs
- if commit.status(ref)
= render_commit_status(commit, ref: ref)
= clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard")
......
......@@ -11,4 +11,4 @@
%li.warning-row.unstyled
#{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
- else
%ul.content-list.table-list= render commits, project: @project, ref: @ref
%ul.content-list= render commits, project: @project, ref: @ref
......@@ -4,7 +4,7 @@
- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits|
%li.commit-header #{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')}
%li.commits-row
%ul.content-list.commit-list.table-list.table-wide
%ul.content-list.commit-list
= render commits, project: project, ref: ref
- if hidden > 0
......
......@@ -26,7 +26,7 @@
= render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.available_project_keys, as: :deploy_key
- else
.settings-message.text-center
No deploy keys from your projects could be found. Create one with the form above
No deploy keys from your projects could be found. Create one with the form above or add existing one below.
- if @deploy_keys.any_available_public_keys_enabled?
%h5.prepend-top-default
Public deploy keys available to any project (#{@deploy_keys.available_public_keys_size})
......
- if can?(current_user, :create_deployment, deployment)
- actions = deployment.manual_actions
- if actions.present?
.inline
.btn-group
.dropdown
%a.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' }
%button.dropdown.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' }
= custom_icon('icon_play')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
......@@ -12,4 +12,3 @@
= link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
= custom_icon('icon_play')
%span= action.name.humanize
......@@ -17,6 +17,6 @@
#{time_ago_with_tooltip(deployment.created_at)}
%td.hidden-xs
.pull-right
.pull-right.btn-group
= render 'projects/deployments/actions', deployment: deployment
= render 'projects/deployments/rollback', deployment: deployment
- email = local_assigns.fetch(:email, false)
- plain = local_assigns.fetch(:plain, false)
- discussions = local_assigns.fetch(:discussions, nil)
- type = line.type
- line_code = diff_file.line_code(line)
%tr.line_holder{ plain ? { class: type} : { class: type, id: line_code } }
- if discussions && !line.meta?
- discussion = discussions[line_code]
%tr.line_holder{ class: type, id: (line_code unless plain) }
- case type
- when 'match'
= diff_match_line line.old_pos, line.new_pos, text: line.text
......@@ -11,12 +14,14 @@
%td.new_line.diff-line-num
%td.line_content.match= line.text
- else
%td.old_line.diff-line-num{ class: type, data: { linenumber: line.old_pos } }
%td.old_line.diff-line-num.js-avatar-container{ class: type, data: { linenumber: line.old_pos } }
- link_text = type == "new" ? " " : line.old_pos
- if plain
= link_text
- else
%a{ href: "##{line_code}", data: { linenumber: link_text } }
- if discussion && !plain
%diff-note-avatars{ "discussion-id" => discussion.id }
%td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
- link_text = type == "old" ? " " : line.new_pos
- if plain
......@@ -29,9 +34,6 @@
- else
= diff_line_content(line.text)
- discussions = local_assigns.fetch(:discussions, nil)
- if discussions && !line.meta?
- discussion = discussions[line_code]
- if discussion
- if discussion
- discussion_expanded = local_assigns.fetch(:discussion_expanded, discussion.expanded?)
= render "discussions/diff_discussion", discussion: discussion, expanded: discussion_expanded
......@@ -4,6 +4,9 @@
- diff_file.parallel_diff_lines.each do |line|
- left = line[:left]
- right = line[:right]
- last_line = right.new_pos if right
- unless @diff_notes_disabled
- discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file)
%tr.line_holder.parallel
- if left
- case left.type
......@@ -15,8 +18,10 @@
- else
- left_line_code = diff_file.line_code(left)
- left_position = diff_file.position(left)
%td.old_line.diff-line-num{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } }
%td.old_line.diff-line-num.js-avatar-container{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } }
%a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } }
- if discussion_left
%diff-note-avatars{ "discussion-id" => discussion_left.id }
%td.line_content.parallel.noteable_line{ class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old') }= diff_line_content(left.text)
- else
%td.old_line.diff-line-num.empty-cell
......@@ -32,15 +37,15 @@
- else
- right_line_code = diff_file.line_code(right)
- right_position = diff_file.position(right)
%td.new_line.diff-line-num{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } }
%td.new_line.diff-line-num.js-avatar-container{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } }
%a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } }
- if discussion_right
%diff-note-avatars{ "discussion-id" => discussion_right.id }
%td.line_content.parallel.noteable_line{ class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new') }= diff_line_content(right.text)
- else
%td.old_line.diff-line-num.empty-cell
%td.line_content.parallel
- unless @diff_notes_disabled
- discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file)
- if discussion_left || discussion_right
= render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right
- if !diff_file.new_file && !diff_file.deleted_file && diff_file.diff_lines.any?
......
= render "projects/settings/head"
.project-edit-container
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
......
......@@ -16,7 +16,7 @@
- if can?(current_user, :create_deployment, @environment) && @environment.can_stop?
= link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post
.deployments-container
.environments-container
- if @deployments.blank?
.blank-state.blank-state-no-icon
%h2.blank-state-title
......
......@@ -21,7 +21,7 @@
selected: f.object.source_project_id
.merge-request-select.dropdown
= f.hidden_field :source_branch
= dropdown_toggle f.object.source_branch || "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
= dropdown_toggle local_assigns.fetch(f.object.source_branch, "Select source branch"), { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
.dropdown-menu.dropdown-menu-selectable.dropdown-source-branch
= dropdown_title("Select source branch")
= dropdown_filter("Search branches")
......@@ -30,7 +30,7 @@
branches: @merge_request.source_branches,
selected: f.object.source_branch
.panel-footer
= icon('spinner spin', class: 'js-source-loading')
.text-center= icon('spinner spin', class: 'js-source-loading')
%ul.list-unstyled.mr_source_commit
.col-md-6
......@@ -60,7 +60,7 @@
branches: @merge_request.target_branches,
selected: f.object.target_branch
.panel-footer
= icon('spinner spin', class: "js-target-loading")
.text-center= icon('spinner spin', class: "js-target-loading")
%ul.list-unstyled.mr_target_commit
- if @merge_request.errors.any?
......
- if @pipeline
.mr-widget-heading
- %w[success success_with_warnings skipped canceled failed running pending].each do |status|
- %w[success success_with_warnings skipped manual canceled failed running pending].each do |status|
.ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) }
%div{ class: "ci-status-icon ci-status-icon-#{status}" }
= link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do
......
......@@ -37,6 +37,8 @@
= render 'projects/merge_requests/widget/open/rebase'
- elsif !@merge_request.mergeable_discussions_state?
= render 'projects/merge_requests/widget/open/unresolved_discussions'
- elsif @pipeline&.blocked?
= render 'projects/merge_requests/widget/open/manual'
- elsif @merge_request.can_be_merged? || resolved_conflicts
= render 'projects/merge_requests/widget/open/accept'
......
%h4
Pipeline blocked
%p
The pipeline for this merge request requires a manual action to proceed.
......@@ -2,7 +2,7 @@
- return if note.cross_reference_not_visible_for?(current_user)
- note_editable = note_editable?(note)
%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} }
%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable, note_id: note.id} }
.timeline-entry-inner
.timeline-icon
%a{ href: user_path(note.author) }
......@@ -30,11 +30,15 @@
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
%resolve-btn{ "discussion-id" => "#{note.discussion_id}",
%resolve-btn{ "project-path" => project_path(note.project),
"discussion-id" => note.discussion_id,
":note-id" => note.id,
":resolved" => note.resolved?,
":can-resolve" => can_resolve,
"resolved-by" => "#{note.resolved_by.try(:name)}",
":author-name" => "'#{j(note.author.name)}'",
"author-avatar" => note.author.avatar_url,
":note-truncated" => "'#{truncate(note.note, length: 17)}'",
":resolved-by" => "'#{j(note.resolved_by.try(:name))}'",
"v-show" => "#{can_resolve || note.resolved?}",
"inline-template" => true,
"ref" => "note_#{note.id}" }
......
- page_title 'Pages'
= render "projects/settings/head"
%h3.page_title
Pages
......
- @stage.statuses.latest.each do |status|
- grouped_statuses = @stage.statuses.latest_ordered.group_by(&:status)
- HasStatus::ORDERED_STATUSES.each do |ordered_status|
- grouped_statuses.fetch(ordered_status, []).each do |status|
%li
= render 'ci/status/dropdown_graph_badge', subject: status
= content_for :sub_nav do
.scrolling-tabs-container.sub-nav-scroll
= render 'shared/nav_scroll'
.nav-links.sub-nav.scrolling-tabs
%ul{ class: container_class }
- can_edit = can?(current_user, :admin_project, @project)
- if can_edit
= nav_link(controller: :projects) do
= link_to edit_project_path(@project), title: 'General' do
%span
General
= nav_link(controller: :members) do
= link_to project_settings_members_path(@project), title: 'Members' do
%span
Members
- if can_edit
= nav_link(controller: :integrations) do
= link_to project_settings_integrations_path(@project), title: 'Integrations' do
%span
Integrations
= nav_link(controller: :repository) do
= link_to namespace_project_settings_repository_path(@project.namespace, @project), title: 'Repository' do
%span
Repository
- if @project.feature_available?(:builds, current_user)
= nav_link(controller: :ci_cd) do
= link_to namespace_project_settings_ci_cd_path(@project.namespace, @project), title: 'CI/CD Pipelines' do
%span
CI/CD Pipelines
= nav_link(controller: :pages) do
= link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages' do
%span
Pages
- page_title "CI/CD Pipelines"
= render "projects/settings/head"
= render 'projects/runners/index'
= render 'projects/variables/index'
......
- page_title 'Integrations'
= render "projects/settings/head"
= render 'projects/hooks/index'
= render 'projects/services/index'
- page_title "Members"
= render "projects/settings/head"
= render "projects/project_members/index"
- if can?(current_user, :admin_project, @project)
......
- page_title "Repository"
= render "projects/settings/head"
= render @deploy_keys
= render "projects/push_rules/index"
= render "projects/mirrors/show"
= render "projects/protected_branches/index"
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.
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.
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