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
...@@ -18,17 +18,18 @@ gem 'pg', '~> 0.18.2', group: :postgres ...@@ -18,17 +18,18 @@ gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.24.0' gem 'rugged', '~> 0.24.0'
# Authentication libraries # Authentication libraries
gem 'devise', '~> 4.2' gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0' gem 'doorkeeper', '~> 4.2.0'
gem 'omniauth', '~> 1.4.2' gem 'doorkeeper-openid_connect', '~> 1.1.0'
gem 'omniauth-auth0', '~> 1.4.1' gem 'omniauth', '~> 1.4.2'
gem 'omniauth-azure-oauth2', '~> 0.0.6' gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-cas3', '~> 1.1.2' gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-facebook', '~> 4.0.0' gem 'omniauth-cas3', '~> 1.1.2'
gem 'omniauth-github', '~> 1.1.1' gem 'omniauth-facebook', '~> 4.0.0'
gem 'omniauth-gitlab', '~> 1.0.2' gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.2'
gem 'omniauth-google-oauth2', '~> 0.4.1' gem 'omniauth-google-oauth2', '~> 0.4.1'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-oauth2-generic', '~> 0.2.2' gem 'omniauth-oauth2-generic', '~> 0.2.2'
gem 'omniauth-saml', '~> 1.7.0' gem 'omniauth-saml', '~> 1.7.0'
gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-shibboleth', '~> 1.2.0'
...@@ -70,9 +71,9 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false ...@@ -70,9 +71,9 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false
gem 'github-linguist', '~> 4.7.0', require: 'linguist' gem 'github-linguist', '~> 4.7.0', require: 'linguist'
# API # API
gem 'grape', '~> 0.19.0' gem 'grape', '~> 0.19.0'
gem 'grape-entity', '~> 0.6.0' gem 'grape-entity', '~> 0.6.0'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors' gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
# Pagination # Pagination
gem 'kaminari', '~> 0.17.0' gem 'kaminari', '~> 0.17.0'
...@@ -112,19 +113,19 @@ gem 'aws-sdk' ...@@ -112,19 +113,19 @@ gem 'aws-sdk'
gem 'faraday_middleware-aws-signers-v4' gem 'faraday_middleware-aws-signers-v4'
# Markdown and HTML processing # Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0' gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie' gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
gem 'gitlab-markup', '~> 1.5.1' gem 'gitlab-markup', '~> 1.5.1'
gem 'redcarpet', '~> 3.4' gem 'redcarpet', '~> 3.4'
gem 'RedCloth', '~> 4.3.2' gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~> 4.2' gem 'rdoc', '~> 4.2'
gem 'org-ruby', '~> 0.9.12' gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0' gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1' gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2' gem 'asciidoctor', '~> 1.5.2'
gem 'asciidoctor-plantuml', '0.0.7' gem 'asciidoctor-plantuml', '0.0.7'
gem 'rouge', '~> 2.0' gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.8' gem 'truncato', '~> 0.7.8'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s # See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM # and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
...@@ -239,11 +240,11 @@ gem 'sass-rails', '~> 5.0.6' ...@@ -239,11 +240,11 @@ gem 'sass-rails', '~> 5.0.6'
gem 'coffee-rails', '~> 4.1.0' gem 'coffee-rails', '~> 4.1.0'
gem 'uglifier', '~> 2.7.2' gem 'uglifier', '~> 2.7.2'
gem 'addressable', '~> 2.3.8' gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0' gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.7' gem 'font-awesome-rails', '~> 4.7'
gem 'gemojione', '~> 3.0' gem 'gemojione', '~> 3.0'
gem 'gon', '~> 6.1.0' gem 'gon', '~> 6.1.0'
gem 'jquery-atwho-rails', '~> 1.3.2' gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'jquery-rails', '~> 4.1.0' gem 'jquery-rails', '~> 4.1.0'
gem 'request_store', '~> 1.3' gem 'request_store', '~> 1.3'
...@@ -289,13 +290,13 @@ group :development, :test do ...@@ -289,13 +290,13 @@ group :development, :test do
gem 'awesome_print', '~> 1.2.0', require: false gem 'awesome_print', '~> 1.2.0', require: false
gem 'fuubar', '~> 2.0.0' gem 'fuubar', '~> 2.0.0'
gem 'database_cleaner', '~> 1.5.0' gem 'database_cleaner', '~> 1.5.0'
gem 'factory_girl_rails', '~> 4.7.0' gem 'factory_girl_rails', '~> 4.7.0'
gem 'rspec-rails', '~> 3.5.0' gem 'rspec-rails', '~> 3.5.0'
gem 'rspec-retry', '~> 0.4.5' gem 'rspec-retry', '~> 0.4.5'
gem 'spinach-rails', '~> 0.2.1' gem 'spinach-rails', '~> 0.2.1'
gem 'spinach-rerun-reporter', '~> 0.0.2' gem 'spinach-rerun-reporter', '~> 0.0.2'
gem 'rspec_profiling', '~> 0.0.5' gem 'rspec_profiling', '~> 0.0.5'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0' gem 'minitest', '~> 5.7.0'
...@@ -303,13 +304,13 @@ group :development, :test do ...@@ -303,13 +304,13 @@ group :development, :test do
# Generate Fake data # Generate Fake data
gem 'ffaker', '~> 2.4' gem 'ffaker', '~> 2.4'
gem 'capybara', '~> 2.6.2' gem 'capybara', '~> 2.6.2'
gem 'capybara-screenshot', '~> 1.0.0' gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.9.0' gem 'poltergeist', '~> 1.9.0'
gem 'spring', '~> 1.7.0' gem 'spring', '~> 1.7.0'
gem 'spring-commands-rspec', '~> 1.0.4' gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.1.0' gem 'spring-commands-spinach', '~> 1.1.0'
gem 'rubocop', '~> 0.47.1', require: false gem 'rubocop', '~> 0.47.1', require: false
gem 'rubocop-rspec', '~> 1.12.0', require: false gem 'rubocop-rspec', '~> 1.12.0', require: false
......
...@@ -86,6 +86,7 @@ GEM ...@@ -86,6 +86,7 @@ GEM
better_errors (1.0.1) better_errors (1.0.1)
coderay (>= 1.0.0) coderay (>= 1.0.0)
erubis (>= 2.6.6) erubis (>= 2.6.6)
bindata (2.3.5)
binding_of_caller (0.7.2) binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1) debug_inspector (>= 0.0.1)
bootstrap-sass (3.3.6) bootstrap-sass (3.3.6)
...@@ -175,6 +176,9 @@ GEM ...@@ -175,6 +176,9 @@ GEM
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.2.0) doorkeeper (4.2.0)
railties (>= 4.2) railties (>= 4.2)
doorkeeper-openid_connect (1.1.2)
doorkeeper (~> 4.0)
json-jwt (~> 1.6)
dropzonejs-rails (0.7.2) dropzonejs-rails (0.7.2)
rails (> 3.1) rails (> 3.1)
elasticsearch (5.0.3) elasticsearch (5.0.3)
...@@ -412,6 +416,12 @@ GEM ...@@ -412,6 +416,12 @@ GEM
railties (>= 4.2.0) railties (>= 4.2.0)
thor (>= 0.14, < 2.0) thor (>= 0.14, < 2.0)
json (1.8.6) json (1.8.6)
json-jwt (1.7.1)
activesupport
bindata
multi_json (>= 1.3)
securecompare
url_safe_base64
json-schema (2.6.2) json-schema (2.6.2)
addressable (~> 2.3.8) addressable (~> 2.3.8)
jwt (1.5.6) jwt (1.5.6)
...@@ -720,6 +730,7 @@ GEM ...@@ -720,6 +730,7 @@ GEM
scss_lint (0.47.1) scss_lint (0.47.1)
rake (>= 0.9, < 11) rake (>= 0.9, < 11)
sass (~> 3.4.15) sass (~> 3.4.15)
securecompare (1.0.0)
seed-fu (2.3.6) seed-fu (2.3.6)
activerecord (>= 3.1) activerecord (>= 3.1)
activesupport (>= 3.1) activesupport (>= 3.1)
...@@ -825,6 +836,7 @@ GEM ...@@ -825,6 +836,7 @@ GEM
get_process_mem (~> 0) get_process_mem (~> 0)
unicorn (>= 4, < 6) unicorn (>= 4, < 6)
uniform_notifier (1.10.0) uniform_notifier (1.10.0)
url_safe_base64 (0.2.2)
validates_hostname (1.0.6) validates_hostname (1.0.6)
activerecord (>= 3.0) activerecord (>= 3.0)
activesupport (>= 3.0) activesupport (>= 3.0)
...@@ -903,6 +915,7 @@ DEPENDENCIES ...@@ -903,6 +915,7 @@ DEPENDENCIES
devise-two-factor (~> 3.0.0) devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0) diffy (~> 3.1.0)
doorkeeper (~> 4.2.0) doorkeeper (~> 4.2.0)
doorkeeper-openid_connect (~> 1.1.0)
dropzonejs-rails (~> 0.7.1) dropzonejs-rails (~> 0.7.1)
elasticsearch-api (= 5.0.3) elasticsearch-api (= 5.0.3)
elasticsearch-model (~> 0.1.9) 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'); ...@@ -11,7 +11,10 @@ const Vue = require('vue');
discussionId: String, discussionId: String,
resolved: Boolean, resolved: Boolean,
canResolve: Boolean, canResolve: Boolean,
resolvedBy: String resolvedBy: String,
authorName: String,
authorAvatar: String,
noteTruncated: String,
}, },
data: function () { data: function () {
return { return {
...@@ -98,7 +101,16 @@ const Vue = require('vue'); ...@@ -98,7 +101,16 @@ const Vue = require('vue');
CommentsStore.delete(this.discussionId, this.noteId); CommentsStore.delete(this.discussionId, this.noteId);
}, },
created: function () { 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); this.note = this.discussion.getNote(this.noteId);
} }
......
...@@ -14,6 +14,7 @@ require('./components/jump_to_discussion'); ...@@ -14,6 +14,7 @@ require('./components/jump_to_discussion');
require('./components/resolve_btn'); require('./components/resolve_btn');
require('./components/resolve_count'); require('./components/resolve_count');
require('./components/resolve_discussion_btn'); require('./components/resolve_discussion_btn');
require('./components/diff_note_avatars');
$(() => { $(() => {
const projectPath = document.querySelector('.merge-request').dataset.projectPath; const projectPath = document.querySelector('.merge-request').dataset.projectPath;
...@@ -25,6 +26,15 @@ $(() => { ...@@ -25,6 +26,15 @@ $(() => {
window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath); window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath);
gl.diffNotesCompileComponents = () => { 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 () { const $components = $(COMPONENT_SELECTOR).filter(function () {
return $(this).closest('resolve-count').length !== 1; 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 { ...@@ -10,8 +10,8 @@ class DiscussionModel {
this.canResolve = false; this.canResolve = false;
} }
createNote (noteId, canResolve, resolved, resolved_by) { createNote (noteObj) {
Vue.set(this.notes, noteId, new NoteModel(this.id, noteId, canResolve, resolved, resolved_by)); Vue.set(this.notes, noteObj.noteId, new NoteModel(this.id, noteObj));
} }
deleteNote (noteId) { deleteNote (noteId) {
......
/* eslint-disable camelcase, no-unused-vars */ /* eslint-disable camelcase, no-unused-vars */
class NoteModel { class NoteModel {
constructor(discussionId, noteId, canResolve, resolved, resolved_by) { constructor(discussionId, noteObj) {
this.discussionId = discussionId; this.discussionId = discussionId;
this.id = noteId; this.id = noteObj.noteId;
this.canResolve = canResolve; this.canResolve = noteObj.canResolve;
this.resolved = resolved; this.resolved = noteObj.resolved;
this.resolved_by = resolved_by; this.resolved_by = noteObj.resolvedBy;
this.authorName = noteObj.authorName;
this.authorAvatar = noteObj.authorAvatar;
this.noteTruncated = noteObj.noteTruncated;
} }
} }
......
...@@ -21,10 +21,10 @@ ...@@ -21,10 +21,10 @@
return discussion; return discussion;
}, },
create: function (discussionId, noteId, canResolve, resolved, resolved_by) { create: function (noteObj) {
const discussion = this.createDiscussion(discussionId); const discussion = this.createDiscussion(noteObj.discussionId);
discussion.createNote(noteId, canResolve, resolved, resolved_by); discussion.createNote(noteObj);
}, },
update: function (discussionId, noteId, resolved, resolved_by) { update: function (discussionId, noteId, resolved, resolved_by) {
const discussion = this.state[discussionId]; const discussion = this.state[discussionId];
......
...@@ -5,7 +5,6 @@ import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make ...@@ -5,7 +5,6 @@ import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make
/* global ShortcutsNavigation */ /* global ShortcutsNavigation */
/* global Build */ /* global Build */
/* global Issuable */ /* global Issuable */
/* global Issue */
/* global ShortcutsIssuable */ /* global ShortcutsIssuable */
/* global ZenMode */ /* global ZenMode */
/* global Milestone */ /* global Milestone */
...@@ -37,6 +36,7 @@ import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make ...@@ -37,6 +36,7 @@ import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make
/* global Shortcuts */ /* global Shortcuts */
/* global WeightSelect */ /* global WeightSelect */
/* global AdminEmailSelect */ /* global AdminEmailSelect */
import Issue from './issue';
import BindInOut from './behaviors/bind_in_out'; import BindInOut from './behaviors/bind_in_out';
import GroupsList from './groups_list'; import GroupsList from './groups_list';
......
...@@ -38,6 +38,9 @@ ...@@ -38,6 +38,9 @@
FilesCommentButton.prototype.render = function(e) { FilesCommentButton.prototype.render = function(e) {
var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button; var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button;
$currentTarget = $(e.currentTarget); $currentTarget = $(e.currentTarget);
if ($currentTarget.hasClass('js-no-comment-btn')) return;
lineContentElement = this.getLineContent($currentTarget); lineContentElement = this.getLineContent($currentTarget);
buttonParentElement = this.getButtonParent($currentTarget); buttonParentElement = this.getButtonParent($currentTarget);
......
...@@ -80,30 +80,48 @@ ...@@ -80,30 +80,48 @@
} }
// Determines the full search query (visual tokens + input) // Determines the full search query (visual tokens + input)
static getSearchQuery() { static getSearchQuery(untilInput = false) {
const tokensContainer = document.querySelector('.tokens-container'); const tokens = [].slice.call(document.querySelectorAll('.tokens-container li'));
const values = []; const values = [];
[].forEach.call(tokensContainer.querySelectorAll('.js-visual-token'), (token) => { if (untilInput) {
const name = token.querySelector('.name'); const inputIndex = _.findIndex(tokens, t => t.classList.contains('input-token'));
const value = token.querySelector('.value'); // Add one to include input-token to the tokens array
const symbol = value && value.dataset.symbol ? value.dataset.symbol : ''; tokens.splice(inputIndex + 1);
let valueText = ''; }
if (value && value.innerText) { tokens.forEach((token) => {
valueText = value.innerText; if (token.classList.contains('js-visual-token')) {
} const name = token.querySelector('.name');
const value = token.querySelector('.value');
if (token.className.indexOf('filtered-search-token') !== -1) { const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`); let valueText = '';
} else {
values.push(name.innerText); if (value && value.innerText) {
valueText = value.innerText;
}
if (token.className.indexOf('filtered-search-token') !== -1) {
values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`);
} else {
values.push(name.innerText);
}
} else if (token.classList.contains('input-token')) {
const { isLastVisualTokenValid } =
gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
const input = document.querySelector('.filtered-search');
const inputValue = input && input.value;
if (isLastVisualTokenValid) {
values.push(inputValue);
} else {
const previous = values.pop();
values.push(`${previous}${inputValue}`);
}
} }
}); });
const input = document.querySelector('.filtered-search');
values.push(input && input.value);
return values.join(' '); return values.join(' ');
} }
......
...@@ -151,7 +151,7 @@ ...@@ -151,7 +151,7 @@
} }
setDropdown() { setDropdown() {
const query = gl.DropdownUtils.getSearchQuery(); const query = gl.DropdownUtils.getSearchQuery(true);
const { lastToken, searchToken } = this.tokenizer.processTokens(query); const { lastToken, searchToken } = this.tokenizer.processTokens(query);
if (this.currentDropdown) { if (this.currentDropdown) {
......
...@@ -178,7 +178,6 @@ ...@@ -178,7 +178,6 @@
if (e.keyCode === 8 || e.keyCode === 46) { if (e.keyCode === 8 || e.keyCode === 46) {
gl.FilteredSearchVisualTokens.removeSelectedToken(); gl.FilteredSearchVisualTokens.removeSelectedToken();
this.handleInputPlaceholder(); this.handleInputPlaceholder();
this.toggleClearSearchButton(); this.toggleClearSearchButton();
} }
} }
...@@ -368,10 +367,13 @@ ...@@ -368,10 +367,13 @@
tokenChange() { tokenChange() {
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
const currentDropdownRef = dropdown.reference;
this.setDropdownWrapper(); if (dropdown) {
currentDropdownRef.dispatchInputEvent(); const currentDropdownRef = dropdown.reference;
this.setDropdownWrapper();
currentDropdownRef.dispatchInputEvent();
}
} }
} }
......
...@@ -5,131 +5,125 @@ require('./flash'); ...@@ -5,131 +5,125 @@ require('./flash');
require('vendor/jquery.waitforimages'); require('vendor/jquery.waitforimages');
require('./task_list'); require('./task_list');
(function() { class Issue {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; constructor() {
if ($('a.btn-close').length) {
this.Issue = (function() { this.taskList = new gl.TaskList({
function Issue() { dataType: 'issue',
this.submitNoteForm = bind(this.submitNoteForm, this); fieldName: 'description',
if ($('a.btn-close').length) { selector: '.detail-page-description',
this.taskList = new gl.TaskList({ onSuccess: (result) => {
dataType: 'issue', document.querySelector('#task_status').innerText = result.task_status;
fieldName: 'description', document.querySelector('#task_status_short').innerText = result.task_status_short;
selector: '.detail-page-description', }
onSuccess: (result) => { });
document.querySelector('#task_status').innerText = result.task_status; Issue.initIssueBtnEventListeners();
document.querySelector('#task_status_short').innerText = result.task_status_short;
}
});
this.initIssueBtnEventListeners();
}
this.initMergeRequests();
this.initRelatedBranches();
this.initCanCreateBranch();
} }
Issue.initMergeRequests();
Issue.initRelatedBranches();
Issue.initCanCreateBranch();
}
Issue.prototype.initIssueBtnEventListeners = function() { static initIssueBtnEventListeners() {
var _this, issueFailMessage; var issueFailMessage;
_this = this; issueFailMessage = 'Unable to update this issue at this time.';
issueFailMessage = 'Unable to update this issue at this time.'; return $('a.btn-close, a.btn-reopen').on('click', function(e) {
return $('a.btn-close, a.btn-reopen').on('click', function(e) { var $this, isClose, shouldSubmit, url;
var $this, isClose, shouldSubmit, url; e.preventDefault();
e.preventDefault(); e.stopImmediatePropagation();
e.stopImmediatePropagation(); $this = $(this);
$this = $(this); isClose = $this.hasClass('btn-close');
isClose = $this.hasClass('btn-close'); shouldSubmit = $this.hasClass('btn-comment');
shouldSubmit = $this.hasClass('btn-comment'); if (shouldSubmit) {
if (shouldSubmit) { Issue.submitNoteForm($this.closest('form'));
_this.submitNoteForm($this.closest('form')); }
} $this.prop('disabled', true);
$this.prop('disabled', true); url = $this.attr('href');
url = $this.attr('href'); return $.ajax({
return $.ajax({ type: 'PUT',
type: 'PUT', url: url,
url: url, error: function(jqXHR, textStatus, errorThrown) {
error: function(jqXHR, textStatus, errorThrown) { var issueStatus;
var issueStatus; issueStatus = isClose ? 'close' : 'open';
issueStatus = isClose ? 'close' : 'open'; return new Flash(issueFailMessage, 'alert');
return new Flash(issueFailMessage, 'alert'); },
}, success: function(data, textStatus, jqXHR) {
success: function(data, textStatus, jqXHR) { if ('id' in data) {
if ('id' in data) { $(document).trigger('issuable:change');
$(document).trigger('issuable:change'); const currentTotal = Number($('.issue_counter').text());
const currentTotal = Number($('.issue_counter').text()); if (isClose) {
if (isClose) { $('a.btn-close').addClass('hidden');
$('a.btn-close').addClass('hidden'); $('a.btn-reopen').removeClass('hidden');
$('a.btn-reopen').removeClass('hidden'); $('div.status-box-closed').removeClass('hidden');
$('div.status-box-closed').removeClass('hidden'); $('div.status-box-open').addClass('hidden');
$('div.status-box-open').addClass('hidden'); $('.issue_counter').text(currentTotal - 1);
$('.issue_counter').text(currentTotal - 1);
} else {
$('a.btn-reopen').addClass('hidden');
$('a.btn-close').removeClass('hidden');
$('div.status-box-closed').addClass('hidden');
$('div.status-box-open').removeClass('hidden');
$('.issue_counter').text(currentTotal + 1);
}
} else { } else {
new Flash(issueFailMessage, 'alert'); $('a.btn-reopen').addClass('hidden');
$('a.btn-close').removeClass('hidden');
$('div.status-box-closed').addClass('hidden');
$('div.status-box-open').removeClass('hidden');
$('.issue_counter').text(currentTotal + 1);
} }
return $this.prop('disabled', false); } else {
new Flash(issueFailMessage, 'alert');
} }
}); return $this.prop('disabled', false);
}
}); });
}; });
}
Issue.prototype.submitNoteForm = function(form) { static submitNoteForm(form) {
var noteText; var noteText;
noteText = form.find("textarea.js-note-text").val(); noteText = form.find("textarea.js-note-text").val();
if (noteText.trim().length > 0) { if (noteText.trim().length > 0) {
return form.submit(); return form.submit();
} }
}; }
Issue.prototype.initMergeRequests = function() { static initMergeRequests() {
var $container; var $container;
$container = $('#merge-requests'); $container = $('#merge-requests');
return $.getJSON($container.data('url')).error(function() { return $.getJSON($container.data('url')).error(function() {
return new Flash('Failed to load referenced merge requests', 'alert'); return new Flash('Failed to load referenced merge requests', 'alert');
}).success(function(data) { }).success(function(data) {
if ('html' in data) { if ('html' in data) {
return $container.html(data.html); return $container.html(data.html);
} }
}); });
}; }
Issue.prototype.initRelatedBranches = function() { static initRelatedBranches() {
var $container; var $container;
$container = $('#related-branches'); $container = $('#related-branches');
return $.getJSON($container.data('url')).error(function() { return $.getJSON($container.data('url')).error(function() {
return new Flash('Failed to load related branches', 'alert'); return new Flash('Failed to load related branches', 'alert');
}).success(function(data) { }).success(function(data) {
if ('html' in data) { if ('html' in data) {
return $container.html(data.html); return $container.html(data.html);
} }
}); });
}; }
Issue.prototype.initCanCreateBranch = function() { static initCanCreateBranch() {
var $container; var $container;
$container = $('#new-branch'); $container = $('#new-branch');
// If the user doesn't have the required permissions the container isn't // If the user doesn't have the required permissions the container isn't
// rendered at all. // rendered at all.
if ($container.length === 0) { if ($container.length === 0) {
return; return;
}
return $.getJSON($container.data('path')).error(function() {
$container.find('.unavailable').show();
return new Flash('Failed to check if a new branch can be created.', 'alert');
}).success(function(data) {
if (data.can_create_branch) {
$container.find('.available').show();
} else {
return $container.find('.unavailable').show();
} }
return $.getJSON($container.data('path')).error(function() { });
$container.find('.unavailable').show(); }
return new Flash('Failed to check if a new branch can be created.', 'alert'); }
}).success(function(data) {
if (data.can_create_branch) {
$container.find('.available').show();
} else {
return $container.find('.unavailable').show();
}
});
};
return Issue; export default Issue;
})();
}).call(window);
...@@ -350,11 +350,11 @@ require('./weight_select'); ...@@ -350,11 +350,11 @@ require('./weight_select');
var notesHolders = $this.closest('.diff-file').find('.notes_holder'); var notesHolders = $this.closest('.diff-file').find('.notes_holder');
$this.toggleClass('active'); $this.toggleClass('active');
if ($this.hasClass('active')) { if ($this.hasClass('active')) {
notesHolders.show().find('.hide').show(); notesHolders.show().find('.hide, .content').show();
} else { } else {
notesHolders.hide(); notesHolders.hide().find('.content').hide();
} }
$this.trigger('blur'); $(document).trigger('toggle.comments');
return e.preventDefault(); return e.preventDefault();
}); });
$document.off('click', '.js-confirm-danger'); $document.off('click', '.js-confirm-danger');
......
...@@ -312,7 +312,7 @@ require('./task_list'); ...@@ -312,7 +312,7 @@ require('./task_list');
*/ */
Notes.prototype.renderDiscussionNote = function(note) { Notes.prototype.renderDiscussionNote = function(note) {
var discussionContainer, form, note_html, row; var discussionContainer, form, note_html, row, lineType, diffAvatarContainer;
if (!this.isNewNote(note)) { if (!this.isNewNote(note)) {
return; return;
} }
...@@ -322,6 +322,8 @@ require('./task_list'); ...@@ -322,6 +322,8 @@ require('./task_list');
form = $("#new-discussion-note-form-" + note.original_discussion_id); form = $("#new-discussion-note-form-" + note.original_discussion_id);
} }
row = form.closest("tr"); 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 = $(note.html);
note_html.renderGFM(); note_html.renderGFM();
// is this the first note of discussion? // is this the first note of discussion?
...@@ -330,10 +332,26 @@ require('./task_list'); ...@@ -330,10 +332,26 @@ require('./task_list');
discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']"); discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']");
} }
if (discussionContainer.length === 0) { if (discussionContainer.length === 0) {
// insert the note and the reply button after the temp row if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) {
row.after(note.diff_discussion_html); // insert the note and the reply button after the temp row
// remove the note (will be added again below) row.after(note.diff_discussion_html);
row.next().find(".note").remove();
// 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 // Before that, the container didn't exist
discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']"); discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
// Add note to 'Changes' page discussions // Add note to 'Changes' page discussions
...@@ -347,14 +365,40 @@ require('./task_list'); ...@@ -347,14 +365,40 @@ require('./task_list');
discussionContainer.append(note_html); discussionContainer.append(note_html);
} }
if (typeof gl.diffNotesCompileComponents !== 'undefined') { if (typeof gl.diffNotesCompileComponents !== 'undefined' && note.discussion_id) {
gl.diffNotesCompileComponents(); gl.diffNotesCompileComponents();
this.renderDiscussionAvatar(diffAvatarContainer, note);
} }
gl.utils.localTimeAgo($('.js-timeago'), false); gl.utils.localTimeAgo($('.js-timeago'), false);
return this.updateNotesCount(1); 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. Called in response the main target form has been successfully submitted.
...@@ -592,9 +636,14 @@ require('./task_list'); ...@@ -592,9 +636,14 @@ require('./task_list');
*/ */
Notes.prototype.removeNote = function(e) { Notes.prototype.removeNote = function(e) {
var noteId; var noteElId, noteId, dataNoteId, $note, lineHolder;
noteId = $(e.currentTarget).closest(".note").attr("id"); $note = $(e.currentTarget).closest('.note');
$(".note[id='" + noteId + "']").each((function(_this) { 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 // 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, // to remove all. Using $(".note[id='noteId']") ensure we get all the notes,
// where $("#noteId") would return only one. // where $("#noteId") would return only one.
...@@ -604,17 +653,26 @@ require('./task_list'); ...@@ -604,17 +653,26 @@ require('./task_list');
notes = note.closest(".notes"); notes = note.closest(".notes");
if (typeof gl.diffNotesCompileComponents !== 'undefined') { if (typeof gl.diffNotesCompileComponents !== 'undefined') {
if (gl.diffNoteApps[noteId]) { if (gl.diffNoteApps[noteElId]) {
gl.diffNoteApps[noteId].$destroy(); gl.diffNoteApps[noteElId].$destroy();
} }
} }
note.remove();
// check if this is the last note for this line // 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 // "Discussions" tab
notes.closest(".timeline-entry").remove(); notes.closest(".timeline-entry").remove();
// "Changes" tab / commit view
notes.closest("tr").remove(); if (!_this.isParallelView() || notesTr.find('.note').length === 0) {
// "Changes" tab / commit view
notesTr.remove();
} else {
notes.closest('.content').empty();
}
} }
return note.remove(); return note.remove();
}; };
...@@ -707,15 +765,16 @@ require('./task_list'); ...@@ -707,15 +765,16 @@ require('./task_list');
*/ */
Notes.prototype.addDiffNote = function(e) { 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(); e.preventDefault();
$link = $(e.currentTarget); $link = $(e.currentTarget || e.target);
row = $link.closest("tr"); row = $link.closest("tr");
nextRow = row.next(); nextRow = row.next();
hasNotes = nextRow.is(".notes_holder"); hasNotes = nextRow.is(".notes_holder");
addForm = false; addForm = false;
notesContentSelector = ".notes_content"; 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>"; 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 // In parallel view, look inside the correct left/right pane
if (this.isParallelView()) { if (this.isParallelView()) {
lineType = $link.data("lineType"); lineType = $link.data("lineType");
...@@ -723,7 +782,9 @@ require('./task_list'); ...@@ -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>"; 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"; notesContentSelector += " .content";
if (hasNotes) { notesContent = nextRow.find(notesContentSelector);
if (hasNotes && !isDiffCommentAvatar) {
nextRow.show(); nextRow.show();
notesContent = nextRow.find(notesContentSelector); notesContent = nextRow.find(notesContentSelector);
if (notesContent.length) { if (notesContent.length) {
...@@ -740,13 +801,21 @@ require('./task_list'); ...@@ -740,13 +801,21 @@ require('./task_list');
} }
} }
} }
} else { } else if (!isDiffCommentAvatar) {
// add a notes row and insert the form // add a notes row and insert the form
row.after(rowCssToAdd); row.after(rowCssToAdd);
nextRow = row.next(); nextRow = row.next();
notesContent = nextRow.find(notesContentSelector); notesContent = nextRow.find(notesContentSelector);
addForm = true; addForm = true;
} else {
nextRow.show();
notesContent.toggle(!notesContent.is(':visible'));
if (!nextRow.find('.content:not(:empty)').is(':visible')) {
nextRow.hide();
}
} }
if (addForm) { if (addForm) {
newForm = this.formClone.clone(); newForm = this.formClone.clone();
newForm.appendTo(notesContent); newForm.appendTo(notesContent);
......
...@@ -4,6 +4,21 @@ ...@@ -4,6 +4,21 @@
&.reset-filters { &.reset-filters {
padding: 7px; 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) { @media (min-width: $screen-sm-min) {
...@@ -34,6 +49,11 @@ ...@@ -34,6 +49,11 @@
display: block; display: block;
margin: 0 0 10px; margin: 0 0 10px;
} }
.dropdown-menu-toggle,
.update-issues-btn .btn {
width: 100%;
}
} }
.filtered-search-container { .filtered-search-container {
...@@ -208,7 +228,15 @@ ...@@ -208,7 +228,15 @@
overflow: auto; 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 { .issues-details-filters {
padding: 0 0 10px; padding: 0 0 10px;
background-color: $white-light; background-color: $white-light;
...@@ -302,4 +330,4 @@ ...@@ -302,4 +330,4 @@
.filter-dropdown-loading { .filter-dropdown-loading {
padding: 8px 16px; padding: 8px 16px;
} }
\ No newline at end of file
...@@ -235,44 +235,6 @@ ul.content-list { ...@@ -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 { .panel > .content-list > li {
padding: $gl-padding-top $gl-padding; padding: $gl-padding-top $gl-padding;
} }
......
...@@ -100,8 +100,7 @@ ...@@ -100,8 +100,7 @@
@media (max-width: $screen-sm-max) { @media (max-width: $screen-sm-max) {
.issues-filters { .issues-filters {
.milestone-filter, .milestone-filter {
.labels-filter {
display: none; display: none;
} }
} }
......
...@@ -294,7 +294,7 @@ ...@@ -294,7 +294,7 @@
.nav-control { .nav-control {
@media (max-width: $screen-sm-max) { @media (max-width: $screen-sm-max) {
margin-right: 75px; margin-right: 2px;
} }
} }
} }
......
...@@ -48,11 +48,3 @@ ...@@ -48,11 +48,3 @@
line-height: inherit; line-height: inherit;
} }
} }
.panel-default {
.table-list-row:last-child {
.table-list-cell {
border-bottom: 0;
}
}
}
...@@ -78,6 +78,7 @@ ...@@ -78,6 +78,7 @@
padding: 5px 10px; padding: 5px 10px;
background-color: $gray-light; background-color: $gray-light;
border-bottom: 1px solid $gray-darker; border-bottom: 1px solid $gray-darker;
border-top: 1px solid $gray-darker;
font-size: 14px; font-size: 14px;
&:first-child { &:first-child {
...@@ -117,10 +118,37 @@ ...@@ -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 { .commit-actions {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
width: 300px;
text-align: right;
font-size: 0; font-size: 0;
} }
......
...@@ -113,6 +113,10 @@ ...@@ -113,6 +113,10 @@
td.line_content.parallel { td.line_content.parallel {
width: 46%; width: 46%;
} }
.add-diff-note {
margin-left: -55px;
}
} }
.old_line, .old_line,
...@@ -490,3 +494,103 @@ ...@@ -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 @@ ...@@ -240,8 +240,7 @@
.commit { .commit {
margin: 0; margin: 0;
padding-top: 2px; padding: 10px 0;
padding-bottom: 2px;
list-style: none; list-style: none;
&:hover { &:hover {
...@@ -409,7 +408,7 @@ ...@@ -409,7 +408,7 @@
} }
.panel-footer { .panel-footer {
padding: 5px 10px; padding: 0;
.btn { .btn {
min-width: auto; min-width: auto;
......
...@@ -331,6 +331,10 @@ ul.notes { ...@@ -331,6 +331,10 @@ ul.notes {
&:hover { &:hover {
color: $gl-link-color; color: $gl-link-color;
}
&:focus,
&:hover {
text-decoration: none; text-decoration: none;
} }
} }
......
...@@ -115,7 +115,7 @@ ...@@ -115,7 +115,7 @@
.table.ci-table { .table.ci-table {
&.builds-page tr { &.builds-page tbody tr {
height: 71px; height: 71px;
} }
......
...@@ -759,6 +759,8 @@ a.allowed-to-push { ...@@ -759,6 +759,8 @@ a.allowed-to-push {
} }
.protected-branches-list { .protected-branches-list {
margin-bottom: 30px;
a { a {
color: $gl-text-color; color: $gl-text-color;
......
...@@ -24,3 +24,14 @@ ...@@ -24,3 +24,14 @@
.service-settings .control-label { .service-settings .control-label {
padding-top: 0; 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 @@ ...@@ -139,18 +139,10 @@
.blob-commit-info { .blob-commit-info {
list-style: none; list-style: none;
background: $gray-light; background: $gray-light;
padding: 6px 0; padding: 16px 16px 16px 6px;
border: 1px solid $border-color; border: 1px solid $border-color;
border-bottom: none; border-bottom: none;
margin: 0; margin: 0;
.table-list-cell {
border-bottom: none;
}
.commit-actions {
width: 260px;
}
} }
#modal-remove-blob > .modal-dialog { width: 850px; } #modal-remove-blob > .modal-dialog { width: 850px; }
......
...@@ -2,7 +2,7 @@ class Admin::ApplicationsController < Admin::ApplicationController ...@@ -2,7 +2,7 @@ class Admin::ApplicationsController < Admin::ApplicationController
include OauthApplications include OauthApplications
before_action :set_application, only: [:show, :edit, :update, :destroy] 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 def index
@applications = Doorkeeper::Application.where("owner_id IS NULL") @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 class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
before_action :authenticate_resource_owner!
layout 'profile' layout 'profile'
# Overriden from Doorkeeper::AuthorizationsController to
# include the call to session.delete
def new def new
if pre_auth.authorizable? if pre_auth.authorizable?
if skip_authorization? || matching_token? if skip_authorization? || matching_token?
...@@ -16,44 +16,4 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController ...@@ -16,44 +16,4 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
render "doorkeeper/authorizations/error" render "doorkeeper/authorizations/error"
end end
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 end
...@@ -4,7 +4,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController ...@@ -4,7 +4,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end end
def create 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 if @personal_access_token.save
flash[:personal_access_token] = @personal_access_token.token flash[:personal_access_token] = @personal_access_token.token
...@@ -16,7 +16,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController ...@@ -16,7 +16,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end end
def revoke 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! if @personal_access_token.revoke!
flash[:notice] = "Revoked personal access token #{@personal_access_token.name}!" flash[:notice] = "Revoked personal access token #{@personal_access_token.name}!"
...@@ -29,14 +29,19 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController ...@@ -29,14 +29,19 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
private private
def finder(options = {})
PersonalAccessTokensFinder.new({ user: current_user, impersonation: false }.merge(options))
end
def personal_access_token_params def personal_access_token_params
params.require(:personal_access_token).permit(:name, :expires_at, scopes: []) params.require(:personal_access_token).permit(:name, :expires_at, scopes: [])
end end
def set_index_vars def set_index_vars
@personal_access_token ||= current_user.personal_access_tokens.build @scopes = Gitlab::Auth::API_SCOPES
@scopes = Gitlab::Auth::SCOPES
@active_personal_access_tokens = current_user.personal_access_tokens.active.order(:expires_at) @personal_access_token = finder.build
@inactive_personal_access_tokens = current_user.personal_access_tokens.inactive @inactive_personal_access_tokens = finder(state: 'inactive').execute
@active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at)
end end
end end
...@@ -10,7 +10,7 @@ module Projects ...@@ -10,7 +10,7 @@ module Projects
.new(@project, current_user: current_user) .new(@project, current_user: current_user)
define_protected_branches define_protected_branches
end end
private private
...@@ -35,16 +35,20 @@ module Projects ...@@ -35,16 +35,20 @@ module Projects
def access_levels_options def access_levels_options
{ {
push_access_levels: { 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: { 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_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 } selected_push_access_levels: @protected_branch.push_access_levels.map { |access_level| access_level.user_id || access_level.access_level }
} }
end end
def open_branches def open_branches
branches = @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } branches = @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } }
{ open_branches: branches } { open_branches: branches }
......
class Projects::TriggersController < Projects::ApplicationController class Projects::TriggersController < Projects::ApplicationController
before_action :authorize_admin_build! 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' layout 'project_settings'
...@@ -8,27 +11,67 @@ class Projects::TriggersController < Projects::ApplicationController ...@@ -8,27 +11,67 @@ class Projects::TriggersController < Projects::ApplicationController
end end
def create def create
@trigger = project.triggers.new @trigger = project.triggers.create(create_params.merge(owner: current_user))
@trigger.save
if @trigger.valid? if @trigger.valid?
redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Trigger was created successfully.' flash[:notice] = 'Trigger was created successfully.'
else else
@triggers = project.triggers.select(&:persisted?) flash[:alert] = 'You could not create a new trigger.'
render action: "show" 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
end end
def destroy def destroy
trigger.destroy if trigger.destroy
flash[:alert] = "Trigger removed" flash[:notice] = "Trigger removed."
else
flash[:alert] = "Could not remove the trigger."
end
redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project) redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project)
end end
private 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 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
end end
...@@ -14,6 +14,8 @@ class UploadsController < ApplicationController ...@@ -14,6 +14,8 @@ class UploadsController < ApplicationController
end end
disposition = uploader.image? ? 'inline' : 'attachment' disposition = uploader.image? ? 'inline' : 'attachment'
expires_in 0.seconds, must_revalidate: true, private: true
send_file uploader.file.path, disposition: disposition send_file uploader.file.path, disposition: disposition
end 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 ...@@ -12,7 +12,7 @@ module BuildsHelper
build_url: namespace_project_build_url(@project.namespace, @project, @build, :json), build_url: namespace_project_build_url(@project.namespace, @project, @build, :json),
build_status: @build.status, build_status: @build.status,
build_stage: @build.stage, build_stage: @build.stage,
log_state: @build.trace_with_state[:state].to_s log_state: ''
} }
end end
......
...@@ -15,6 +15,8 @@ module CiStatusHelper ...@@ -15,6 +15,8 @@ module CiStatusHelper
'passed' 'passed'
when 'success_with_warnings' when 'success_with_warnings'
'passed with warnings' 'passed with warnings'
when 'manual'
'waiting for manual action'
else else
status status
end end
...@@ -48,6 +50,8 @@ module CiStatusHelper ...@@ -48,6 +50,8 @@ module CiStatusHelper
'icon_status_created' 'icon_status_created'
when 'skipped' when 'skipped'
'icon_status_skipped' 'icon_status_skipped'
when 'manual'
'icon_status_manual'
else else
'icon_status_canceled' 'icon_status_canceled'
end end
......
...@@ -162,7 +162,12 @@ module EventsHelper ...@@ -162,7 +162,12 @@ module EventsHelper
def event_note(text, options = {}) def event_note(text, options = {})
text = first_line_in_markdown(text, 150, 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 end
def event_commit_title(message) def event_commit_title(message)
......
...@@ -36,7 +36,7 @@ module PreferencesHelper ...@@ -36,7 +36,7 @@ module PreferencesHelper
def project_view_choices def project_view_choices
[ [
['Files and Readme (default)', :files], ['Files and Readme (default)', :files],
['Activity view', :activity] ['Activity', :activity]
] ]
end end
......
...@@ -52,7 +52,7 @@ module SortingHelper ...@@ -52,7 +52,7 @@ module SortingHelper
end end
def sort_title_priority def sort_title_priority
'Priority' 'Label priority'
end end
def sort_title_oldest_updated def sort_title_oldest_updated
......
class ChatTeam < ActiveRecord::Base class ChatTeam < ActiveRecord::Base
validates :team_id, presence: true validates :team_id, presence: true
validates :namespace, uniqueness: true
belongs_to :namespace belongs_to :namespace
end end
...@@ -518,6 +518,27 @@ module Ci ...@@ -518,6 +518,27 @@ module Ci
] ]
end 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 def credentials
Gitlab::Ci::Build::Credentials::Factory.new(self).create! Gitlab::Ci::Build::Credentials::Factory.new(self).create!
end end
...@@ -544,10 +565,35 @@ module Ci ...@@ -544,10 +565,35 @@ module Ci
@unscoped_project ||= Project.unscoped.find_by(id: gl_project_id) @unscoped_project ||= Project.unscoped.find_by(id: gl_project_id)
end end
CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
def predefined_variables def predefined_variables
variables = [ variables = [
{ key: 'CI', value: 'true', public: true }, { key: 'CI', value: 'true', public: true },
{ key: 'GITLAB_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_ID', value: id.to_s, public: true },
{ key: 'CI_BUILD_TOKEN', value: token, public: false }, { key: 'CI_BUILD_TOKEN', value: token, public: false },
{ key: 'CI_BUILD_REF', value: sha, public: true }, { key: 'CI_BUILD_REF', value: sha, public: true },
...@@ -555,14 +601,12 @@ module Ci ...@@ -555,14 +601,12 @@ module Ci
{ key: 'CI_BUILD_REF_NAME', value: ref, public: true }, { key: 'CI_BUILD_REF_NAME', value: ref, public: true },
{ key: 'CI_BUILD_REF_SLUG', value: ref_slug, public: true }, { key: 'CI_BUILD_REF_SLUG', value: ref_slug, public: true },
{ key: 'CI_BUILD_NAME', value: name, public: true }, { key: 'CI_BUILD_NAME', value: name, public: true },
{ key: 'CI_BUILD_STAGE', value: stage, 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 }
] ]
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_TAG", value: ref, public: true } if tag?
variables << { key: 'CI_BUILD_MANUAL', value: 'true', public: true } if action? variables << { key: "CI_BUILD_TRIGGERED", value: 'true', public: true } if trigger_request
variables << { key: "CI_BUILD_MANUAL", value: 'true', public: true } if action?
variables variables
end end
......
...@@ -29,8 +29,12 @@ module Ci ...@@ -29,8 +29,12 @@ module Ci
token[0...4] token[0...4]
end end
def can_show_token?(user) def legacy?
owner.blank? || owner == user self.owner_id.blank?
end
def can_access_project?
self.owner_id.blank? || Ability.allowed?(self.owner, :create_build, project)
end end
end end
end end
...@@ -7,7 +7,7 @@ module HasStatus ...@@ -7,7 +7,7 @@ module HasStatus
STARTED_STATUSES = %w[running success failed skipped manual].freeze STARTED_STATUSES = %w[running success failed skipped manual].freeze
ACTIVE_STATUSES = %w[pending running].freeze ACTIVE_STATUSES = %w[pending running].freeze
COMPLETED_STATUSES = %w[success failed canceled skipped].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 class_methods do
def status_sql def status_sql
......
class Geo::BaseRegistry < ActiveRecord::Base class Geo::BaseRegistry < ActiveRecord::Base
self.abstract_class = true 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 establish_connection Rails.configuration.geo_database
end end
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 :resource_owner, class_name: 'User'
belongs_to :application, class_name: 'Doorkeeper::Application' belongs_to :application, class_name: 'Doorkeeper::Application'
end end
class PersonalAccessToken < ActiveRecord::Base class PersonalAccessToken < ActiveRecord::Base
include Expirable
include TokenAuthenticatable include TokenAuthenticatable
add_authentication_token_field :token add_authentication_token_field :token
...@@ -6,17 +7,30 @@ class PersonalAccessToken < ActiveRecord::Base ...@@ -6,17 +7,30 @@ class PersonalAccessToken < ActiveRecord::Base
belongs_to :user 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 :inactive, -> { where("revoked = true OR expires_at < NOW()") }
scope :with_impersonation, -> { where(impersonation: true) }
scope :without_impersonation, -> { where(impersonation: false) }
def self.generate(params) validates :scopes, presence: true
personal_access_token = self.new(params) validate :validate_api_scopes
personal_access_token.ensure_token
personal_access_token
end
def revoke! def revoke!
self.revoked = true self.revoked = true
self.save self.save
end 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 end
...@@ -36,7 +36,7 @@ class KubernetesService < DeploymentService ...@@ -36,7 +36,7 @@ class KubernetesService < DeploymentService
def initialize_properties def initialize_properties
if properties.nil? if properties.nil?
self.properties = {} self.properties = {}
self.namespace = project.path if project.present? self.namespace = "#{project.path}-#{project.id}" if project.present?
end end
end end
......
...@@ -320,11 +320,13 @@ class Repository ...@@ -320,11 +320,13 @@ class Repository
if !branch_name || branch_name == root_ref if !branch_name || branch_name == root_ref
branches.each do |branch| branches.each do |branch|
cache.expire(:"diverging_commit_counts_#{branch.name}") cache.expire(:"diverging_commit_counts_#{branch.name}")
cache.expire(:"commit_count_#{branch.name}")
end end
# In case a commit is pushed to a non-root branch we only have to flush the # In case a commit is pushed to a non-root branch we only have to flush the
# cache for said branch. # cache for said branch.
else else
cache.expire(:"diverging_commit_counts_#{branch_name}") cache.expire(:"diverging_commit_counts_#{branch_name}")
cache.expire(:"commit_count_#{branch_name}")
end end
end end
...@@ -504,6 +506,16 @@ class Repository ...@@ -504,6 +506,16 @@ class Repository
end end
cache_method :commit_count, fallback: 0 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 def branch_names
branches.map(&:name) branches.map(&:name)
end end
......
...@@ -345,8 +345,7 @@ class User < ActiveRecord::Base ...@@ -345,8 +345,7 @@ class User < ActiveRecord::Base
end end
def find_by_personal_access_token(token_string) def find_by_personal_access_token(token_string)
personal_access_token = PersonalAccessToken.active.find_by_token(token_string) if token_string PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user
personal_access_token&.user
end end
# Returns a user for the given SSH key. # 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 module Ci
# This class responsible for assigning # This class responsible for assigning
# proper pending build to runner on runner API request # proper pending build to runner on runner API request
class RegisterBuildService class RegisterJobService
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
prepend EE::Ci::RegisterBuildService prepend EE::Ci::RegisterJobService
attr_reader :runner attr_reader :runner
......
module EE module EE
module Ci module Ci
# RegisterBuildService EE mixin # RegisterJobService EE mixin
# #
# This module is intended to encapsulate EE-specific service logic # This module is intended to encapsulate EE-specific service logic
# and be included in the `RegisterBuildService` service # and be included in the `RegisterJobService` service
module RegisterBuildService module RegisterJobService
extend ActiveSupport::Concern extend ActiveSupport::Concern
def builds_for_shared_runner def builds_for_shared_runner
......
...@@ -104,6 +104,7 @@ class GitPushService < BaseService ...@@ -104,6 +104,7 @@ class GitPushService < BaseService
.perform_async(@project.id, current_user.id, params[:oldrev], params[:newrev], params[:ref]) .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) 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) EventCreateService.new.push(@project, current_user, build_push_data)
@project.execute_hooks(build_push_data.dup, :push_hooks) @project.execute_hooks(build_push_data.dup, :push_hooks)
......
...@@ -35,12 +35,21 @@ class NamespaceValidator < ActiveModel::EachValidator ...@@ -35,12 +35,21 @@ class NamespaceValidator < ActiveModel::EachValidator
users users
].freeze ].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) def self.valid?(value)
!reserved?(value) && follow_format?(value) !reserved?(value) && follow_format?(value)
end end
def self.reserved?(value) def self.reserved?(value, strict: false)
RESERVED.include?(value) if strict
STRICT_RESERVED.include?(value)
else
RESERVED.include?(value)
end
end end
def self.follow_format?(value) def self.follow_format?(value)
...@@ -54,7 +63,9 @@ class NamespaceValidator < ActiveModel::EachValidator ...@@ -54,7 +63,9 @@ class NamespaceValidator < ActiveModel::EachValidator
record.errors.add(attribute, Gitlab::Regex.namespace_regex_message) record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
end 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") record.errors.add(attribute, "#{value} is a reserved name")
end end
end end
......
...@@ -14,10 +14,8 @@ class ProjectPathValidator < ActiveModel::EachValidator ...@@ -14,10 +14,8 @@ class ProjectPathValidator < ActiveModel::EachValidator
# without tree as reserved name routing can match 'group/project' as group name, # without tree as reserved name routing can match 'group/project' as group name,
# 'tree' as project name and 'deploy_keys' as route. # 'tree' as project name and 'deploy_keys' as route.
# #
RESERVED = (NamespaceValidator::RESERVED - RESERVED = (NamespaceValidator::STRICT_RESERVED -
%w[dashboard help ci admin search notes services assets profile public] + %w[dashboard help ci admin search notes services assets profile public]).freeze
%w[tree commits wikis new edit create update logs_tree
preview blob blame raw files create_dir find_file]).freeze
def self.valid?(value) def self.valid?(value)
!reserved?(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 @@ ...@@ -23,4 +23,6 @@
= link_to "SSH keys", keys_admin_user_path(@user) = link_to "SSH keys", keys_admin_user_path(@user)
= nav_link(controller: :identities) do = nav_link(controller: :identities) do
= link_to "Identities", admin_user_identities_path(@user) = 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 .append-bottom-default
...@@ -6,10 +6,8 @@ ...@@ -6,10 +6,8 @@
.top-area .top-area
= render 'shared/issuable/nav', type: :issues = render 'shared/issuable/nav', type: :issues
.nav-controls .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') = icon('rss')
%span.icon-label
Subscribe
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
= render 'shared/issuable/filter', type: :issues = render 'shared/issuable/filter', type: :issues
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
- if @last_push - if @last_push
= render "events/event_last_push", event: @last_push = render "events/event_last_push", event: @last_push
- if @projects.any? - if @projects.any? || params[:filter_projects]
= render 'projects' = render 'projects'
- else - else
%h3 You don't have starred projects yet %h3 You don't have starred projects yet
......
- if inject_u2f_api? - if inject_u2f_api?
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_tag('u2f.js') = page_specific_javascript_bundle_tag('u2f')
%div %div
= render 'devise/shared/tab_single', tab_title: 'Two-Factor Authentication' = render 'devise/shared/tab_single', tab_title: 'Two-Factor Authentication'
......
...@@ -2,5 +2,5 @@ ...@@ -2,5 +2,5 @@
%tr.notes_holder{ class: ('hide' unless expanded) } %tr.notes_holder{ class: ('hide' unless expanded) }
%td.notes_line{ colspan: 2 } %td.notes_line{ colspan: 2 }
%td.notes_content %td.notes_content
.content .content{ class: ('hide' unless expanded) }
= render "discussions/notes", discussion: discussion = render "discussions/notes", discussion: discussion
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
= hidden_field_tag :state, @pre_auth.state = hidden_field_tag :state, @pre_auth.state
= hidden_field_tag :response_type, @pre_auth.response_type = hidden_field_tag :response_type, @pre_auth.response_type
= hidden_field_tag :scope, @pre_auth.scope = hidden_field_tag :scope, @pre_auth.scope
= hidden_field_tag :nonce, @pre_auth.nonce
= submit_tag "Authorize", class: "btn btn-success wide pull-left" = submit_tag "Authorize", class: "btn btn-success wide pull-left"
= form_tag oauth_authorization_path, method: :delete do = form_tag oauth_authorization_path, method: :delete do
= hidden_field_tag :client_id, @pre_auth.client.uid = hidden_field_tag :client_id, @pre_auth.client.uid
...@@ -34,4 +35,5 @@ ...@@ -34,4 +35,5 @@
= hidden_field_tag :state, @pre_auth.state = hidden_field_tag :state, @pre_auth.state
= hidden_field_tag :response_type, @pre_auth.response_type = hidden_field_tag :response_type, @pre_auth.response_type
= hidden_field_tag :scope, @pre_auth.scope = hidden_field_tag :scope, @pre_auth.scope
= hidden_field_tag :nonce, @pre_auth.nonce
= submit_tag "Deny", class: "btn btn-danger prepend-left-10" = submit_tag "Deny", class: "btn btn-danger prepend-left-10"
- if current_user - can_edit = can?(current_user, :admin_project, @project)
.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
.scrolling-tabs-container{ class: nav_control_class } .scrolling-tabs-container{ class: nav_control_class }
.fade-left .fade-left
= icon('angle-left') = icon('angle-left')
...@@ -71,6 +55,17 @@ ...@@ -71,6 +55,17 @@
%span %span
Snippets 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 -# Shortcut to Project > Activity
%li.hidden %li.hidden
= link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do = 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 @@ ...@@ -24,80 +24,11 @@
%hr %hr
%h5.prepend-top-0 = render "shared/personal_access_tokens_form", path: profile_personal_access_tokens_path, impersonation: false, token: @personal_access_token, scopes: @scopes
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_table", impersonation: false, active_tokens: @active_personal_access_tokens, inactive_tokens: @inactive_personal_access_tokens
:javascript :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() { $("#created-personal-access-token").click(function() {
this.select(); this.select();
}); });
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
- if inject_u2f_api? - if inject_u2f_api?
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_tag('u2f.js') = page_specific_javascript_bundle_tag('u2f')
.row.prepend-top-default .row.prepend-top-default
.col-lg-3 .col-lg-3
...@@ -96,4 +96,3 @@ ...@@ -96,4 +96,3 @@
:javascript :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>"; 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); $(".flash-alert").append(button);
- @no_container = true - @no_container = true
= render "projects/head"
%div{ class: container_class } %div{ class: container_class }
.nav-block.activity-filter-block .nav-block.activity-filter-block
......
- page_title "Activity" - page_title "Activity"
= render "projects/head"
= render 'projects/last_push' = render 'projects/last_push'
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
= link_to title, '#' = link_to title, '#'
= render_lock_icon(part_path) = 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) - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path)
= render blob_commit, project: @project, ref: @ref = render blob_commit, project: @project, ref: @ref
......
- @no_container = true - @no_container = true
- page_title "#{@build.name} (##{@build.id})", "Jobs" - page_title "#{@build.name} (##{@build.id})", "Jobs"
- trace_with_state = @build.trace_with_state
= render "projects/pipelines/head", build_subnav: true = render "projects/pipelines/head", build_subnav: true
%div{ class: container_class } %div{ class: container_class }
......
...@@ -34,8 +34,9 @@ ...@@ -34,8 +34,9 @@
= revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false) = revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false)
%li.clearfix %li.clearfix
= cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false) = cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false)
%li.clearfix - if can_collaborate_with_project?
= link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit) %li.clearfix
= link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit)
%li.divider %li.divider
%li.dropdown-header %li.dropdown-header
Download Download
......
...@@ -9,33 +9,34 @@ ...@@ -9,33 +9,34 @@
- cache_key.push(commit.status(ref)) if commit.status(ref) - cache_key.push(commit.status(ref)) if commit.status(ref)
= cache(cache_key, expires_in: 1.day) do = 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) = author_avatar(commit, size: 36)
.table-list-cell.commit-content .commit-detail
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message item-title" .commit-content
%span.commit-row-message.visible-xs-inline = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message item-title"
&middot; %span.commit-row-message.visible-xs-inline
= commit.short_id &middot;
- if commit.status(ref) = commit.short_id
.visible-xs-inline - if commit.status(ref)
= render_commit_status(commit, ref: ref) .visible-xs-inline
- if commit.description? = render_commit_status(commit, ref: ref)
%a.text-expander.hidden-xs.js-toggle-button ... - if commit.description?
%a.text-expander.hidden-xs.js-toggle-button ...
- if commit.description? - if commit.description?
%pre.commit-row-description.js-toggle-content %pre.commit-row-description.js-toggle-content
= preserve(markdown(commit.description, pipeline: :single_line, author: commit.author)) = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author))
.commiter .commiter
= commit_author_link(commit, avatar: false, size: 24) = commit_author_link(commit, avatar: false, size: 24)
committed committed
#{time_ago_with_tooltip(commit.committed_date)} #{time_ago_with_tooltip(commit.committed_date)}
.table-list-cell.commit-actions.hidden-xs .commit-actions.flex-row.hidden-xs
- if commit.status(ref) - if commit.status(ref)
= render_commit_status(commit, ref: ref) = render_commit_status(commit, ref: ref)
= clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard") = clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard")
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent" = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent"
= link_to_browse_code(project, commit) = link_to_browse_code(project, commit)
...@@ -11,4 +11,4 @@ ...@@ -11,4 +11,4 @@
%li.warning-row.unstyled %li.warning-row.unstyled
#{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues. #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
- else - 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 @@ ...@@ -4,7 +4,7 @@
- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits| - 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.commit-header #{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')}
%li.commits-row %li.commits-row
%ul.content-list.commit-list.table-list.table-wide %ul.content-list.commit-list
= render commits, project: project, ref: ref = render commits, project: project, ref: ref
- if hidden > 0 - if hidden > 0
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
= render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.available_project_keys, as: :deploy_key = render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.available_project_keys, as: :deploy_key
- else - else
.settings-message.text-center .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? - if @deploy_keys.any_available_public_keys_enabled?
%h5.prepend-top-default %h5.prepend-top-default
Public deploy keys available to any project (#{@deploy_keys.available_public_keys_size}) Public deploy keys available to any project (#{@deploy_keys.available_public_keys_size})
......
- if can?(current_user, :create_deployment, deployment) - if can?(current_user, :create_deployment, deployment)
- actions = deployment.manual_actions - actions = deployment.manual_actions
- if actions.present? - if actions.present?
.inline .btn-group
.dropdown .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') = custom_icon('icon_play')
= icon('caret-down') = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
...@@ -12,4 +12,3 @@ ...@@ -12,4 +12,3 @@
= link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
= custom_icon('icon_play') = custom_icon('icon_play')
%span= action.name.humanize %span= action.name.humanize
...@@ -17,6 +17,6 @@ ...@@ -17,6 +17,6 @@
#{time_ago_with_tooltip(deployment.created_at)} #{time_ago_with_tooltip(deployment.created_at)}
%td.hidden-xs %td.hidden-xs
.pull-right .pull-right.btn-group
= render 'projects/deployments/actions', deployment: deployment = render 'projects/deployments/actions', deployment: deployment
= render 'projects/deployments/rollback', deployment: deployment = render 'projects/deployments/rollback', deployment: deployment
- email = local_assigns.fetch(:email, false) - email = local_assigns.fetch(:email, false)
- plain = local_assigns.fetch(:plain, false) - plain = local_assigns.fetch(:plain, false)
- discussions = local_assigns.fetch(:discussions, nil)
- type = line.type - type = line.type
- line_code = diff_file.line_code(line) - 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 - case type
- when 'match' - when 'match'
= diff_match_line line.old_pos, line.new_pos, text: line.text = diff_match_line line.old_pos, line.new_pos, text: line.text
...@@ -11,12 +14,14 @@ ...@@ -11,12 +14,14 @@
%td.new_line.diff-line-num %td.new_line.diff-line-num
%td.line_content.match= line.text %td.line_content.match= line.text
- else - 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 - link_text = type == "new" ? " " : line.old_pos
- if plain - if plain
= link_text = link_text
- else - else
%a{ href: "##{line_code}", data: { linenumber: link_text } } %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 } } %td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
- link_text = type == "old" ? " " : line.new_pos - link_text = type == "old" ? " " : line.new_pos
- if plain - if plain
...@@ -29,9 +34,6 @@ ...@@ -29,9 +34,6 @@
- else - else
= diff_line_content(line.text) = diff_line_content(line.text)
- discussions = local_assigns.fetch(:discussions, nil) - if discussion
- if discussions && !line.meta? - discussion_expanded = local_assigns.fetch(:discussion_expanded, discussion.expanded?)
- discussion = discussions[line_code] = render "discussions/diff_discussion", discussion: discussion, expanded: discussion_expanded
- if discussion
- discussion_expanded = local_assigns.fetch(:discussion_expanded, discussion.expanded?)
= render "discussions/diff_discussion", discussion: discussion, expanded: discussion_expanded
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
- diff_file.parallel_diff_lines.each do |line| - diff_file.parallel_diff_lines.each do |line|
- left = line[:left] - left = line[:left]
- right = line[:right] - 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 %tr.line_holder.parallel
- if left - if left
- case left.type - case left.type
...@@ -15,8 +18,10 @@ ...@@ -15,8 +18,10 @@
- else - else
- left_line_code = diff_file.line_code(left) - left_line_code = diff_file.line_code(left)
- left_position = diff_file.position(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 } } %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) %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 - else
%td.old_line.diff-line-num.empty-cell %td.old_line.diff-line-num.empty-cell
...@@ -32,17 +37,17 @@ ...@@ -32,17 +37,17 @@
- else - else
- right_line_code = diff_file.line_code(right) - right_line_code = diff_file.line_code(right)
- right_position = diff_file.position(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 } } %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) %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 - else
%td.old_line.diff-line-num.empty-cell %td.old_line.diff-line-num.empty-cell
%td.line_content.parallel %td.line_content.parallel
- unless @diff_notes_disabled - if discussion_left || discussion_right
- discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file) = render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right
- 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? - if !diff_file.new_file && !diff_file.deleted_file && diff_file.diff_lines.any?
- last_line = diff_file.diff_lines.last - last_line = diff_file.diff_lines.last
- if last_line.new_pos < total_lines - if last_line.new_pos < total_lines
......
= render "projects/settings/head"
.project-edit-container .project-edit-container
.row.prepend-top-default .row.prepend-top-default
.col-lg-3.profile-settings-sidebar .col-lg-3.profile-settings-sidebar
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
- if can?(current_user, :create_deployment, @environment) && @environment.can_stop? - 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 = 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? - if @deployments.blank?
.blank-state.blank-state-no-icon .blank-state.blank-state-no-icon
%h2.blank-state-title %h2.blank-state-title
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
selected: f.object.source_project_id selected: f.object.source_project_id
.merge-request-select.dropdown .merge-request-select.dropdown
= f.hidden_field :source_branch = 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-menu.dropdown-menu-selectable.dropdown-source-branch
= dropdown_title("Select source branch") = dropdown_title("Select source branch")
= dropdown_filter("Search branches") = dropdown_filter("Search branches")
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
branches: @merge_request.source_branches, branches: @merge_request.source_branches,
selected: f.object.source_branch selected: f.object.source_branch
.panel-footer .panel-footer
= icon('spinner spin', class: 'js-source-loading') .text-center= icon('spinner spin', class: 'js-source-loading')
%ul.list-unstyled.mr_source_commit %ul.list-unstyled.mr_source_commit
.col-md-6 .col-md-6
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
branches: @merge_request.target_branches, branches: @merge_request.target_branches,
selected: f.object.target_branch selected: f.object.target_branch
.panel-footer .panel-footer
= icon('spinner spin', class: "js-target-loading") .text-center= icon('spinner spin', class: "js-target-loading")
%ul.list-unstyled.mr_target_commit %ul.list-unstyled.mr_target_commit
- if @merge_request.errors.any? - if @merge_request.errors.any?
......
- if @pipeline - if @pipeline
.mr-widget-heading .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) } .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) }
%div{ class: "ci-status-icon ci-status-icon-#{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 = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do
......
...@@ -37,6 +37,8 @@ ...@@ -37,6 +37,8 @@
= render 'projects/merge_requests/widget/open/rebase' = render 'projects/merge_requests/widget/open/rebase'
- elsif !@merge_request.mergeable_discussions_state? - elsif !@merge_request.mergeable_discussions_state?
= render 'projects/merge_requests/widget/open/unresolved_discussions' = 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 - elsif @merge_request.can_be_merged? || resolved_conflicts
= render 'projects/merge_requests/widget/open/accept' = 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 @@ ...@@ -2,7 +2,7 @@
- return if note.cross_reference_not_visible_for?(current_user) - return if note.cross_reference_not_visible_for?(current_user)
- note_editable = note_editable?(note) - 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-entry-inner
.timeline-icon .timeline-icon
%a{ href: user_path(note.author) } %a{ href: user_path(note.author) }
...@@ -30,11 +30,15 @@ ...@@ -30,11 +30,15 @@
- if note.resolvable? - if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note) - 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, ":note-id" => note.id,
":resolved" => note.resolved?, ":resolved" => note.resolved?,
":can-resolve" => can_resolve, ":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?}", "v-show" => "#{can_resolve || note.resolved?}",
"inline-template" => true, "inline-template" => true,
"ref" => "note_#{note.id}" } "ref" => "note_#{note.id}" }
......
- page_title 'Pages' - page_title 'Pages'
= render "projects/settings/head"
%h3.page_title %h3.page_title
Pages Pages
......
- @stage.statuses.latest.each do |status| - grouped_statuses = @stage.statuses.latest_ordered.group_by(&:status)
%li - HasStatus::ORDERED_STATUSES.each do |ordered_status|
= render 'ci/status/dropdown_graph_badge', subject: 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" - page_title "CI/CD Pipelines"
= render "projects/settings/head"
= render 'projects/runners/index' = render 'projects/runners/index'
= render 'projects/variables/index' = render 'projects/variables/index'
......
- page_title 'Integrations' - page_title 'Integrations'
= render "projects/settings/head"
= render 'projects/hooks/index' = render 'projects/hooks/index'
= render 'projects/services/index' = render 'projects/services/index'
- page_title "Members" - page_title "Members"
= render "projects/settings/head"
= render "projects/project_members/index" = render "projects/project_members/index"
- if can?(current_user, :admin_project, @project) - if can?(current_user, :admin_project, @project)
......
- page_title "Repository" - page_title "Repository"
= render "projects/settings/head"
= render @deploy_keys = render @deploy_keys
= render "projects/push_rules/index" = render "projects/push_rules/index"
= render "projects/mirrors/show" = render "projects/mirrors/show"
= render "projects/protected_branches/index" = 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