Commit 779d6e80 authored by Rémy Coutable's avatar Rémy Coutable

Merge remote-tracking branch 'ce/master' into rc/ce-upstream

Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parents d5549025 f08d756e
......@@ -15,6 +15,7 @@
"filenames"
],
"rules": {
"filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"]
"filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"],
"no-multiple-empty-lines": ["error", { "max": 1 }]
}
}
......@@ -46,7 +46,7 @@ linters:
max: 80
MultilinePipe:
enabled: false
enabled: true
MultilineScript:
enabled: true
......@@ -77,7 +77,7 @@ linters:
- Style/WhileUntilModifier
RubyComments:
enabled: false
enabled: true
SpaceBeforeScript:
enabled: true
......@@ -97,7 +97,7 @@ linters:
enabled: true
UnnecessaryInterpolation:
enabled: false
enabled: true
UnnecessaryStringOutput:
enabled: false
enabled: true
......@@ -264,6 +264,9 @@
case 'projects:artifacts:browse':
new BuildArtifacts();
break;
case 'help:index':
gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge'));
break;
case 'search:show':
new Search();
break;
......
......@@ -58,6 +58,7 @@ var CustomEvent = require('./custom_event_polyfill');
var utils = require('./utils');
var DropDown = function(list) {
this.currentIndex = 0;
this.hidden = true;
this.list = list;
this.items = [];
......@@ -164,15 +165,21 @@ Object.assign(DropDown.prototype, {
},
show: function() {
// debugger
this.list.style.display = 'block';
this.hidden = false;
if (this.hidden) {
// debugger
this.list.style.display = 'block';
this.currentIndex = 0;
this.hidden = false;
}
},
hide: function() {
// debugger
this.list.style.display = 'none';
this.hidden = true;
if (!this.hidden) {
// debugger
this.list.style.display = 'none';
this.currentIndex = 0;
this.hidden = true;
}
},
destroy: function() {
......@@ -478,6 +485,8 @@ Object.assign(HookInput.prototype, {
this.input = function input(e) {
if(self.hasRemovedEvents) return;
self.list.show();
var inputEvent = new CustomEvent('input.dl', {
detail: {
hook: self,
......@@ -487,7 +496,6 @@ Object.assign(HookInput.prototype, {
cancelable: true
});
e.target.dispatchEvent(inputEvent);
self.list.show();
}
this.keyup = function keyup(e) {
......@@ -503,6 +511,8 @@ Object.assign(HookInput.prototype, {
}
function keyEvent(e, keyEventName){
self.list.show();
var keyEvent = new CustomEvent(keyEventName, {
detail: {
hook: self,
......@@ -514,7 +524,6 @@ Object.assign(HookInput.prototype, {
cancelable: true
});
e.target.dispatchEvent(keyEvent);
self.list.show();
}
this.events = this.events || {};
......@@ -572,24 +581,43 @@ require('./window')(function(w){
module.exports = function(){
var currentKey;
var currentFocus;
var currentIndex = 0;
var isUpArrow = false;
var isDownArrow = false;
var removeHighlight = function removeHighlight(list) {
var listItems = list.list.querySelectorAll('li');
var listItems = Array.prototype.slice.call(list.list.querySelectorAll('li:not(.divider)'), 0);
var listItemsTmp = [];
for(var i = 0; i < listItems.length; i++) {
listItems[i].classList.remove('dropdown-active');
var listItem = listItems[i];
listItem.classList.remove('dropdown-active');
if (listItem.style.display !== 'none') {
listItemsTmp.push(listItem);
}
}
return listItems;
return listItemsTmp;
};
var setMenuForArrows = function setMenuForArrows(list) {
var listItems = removeHighlight(list);
if(currentIndex>0){
if(!listItems[currentIndex-1]){
currentIndex = currentIndex-1;
if(list.currentIndex>0){
if(!listItems[list.currentIndex-1]){
list.currentIndex = list.currentIndex-1;
}
if (listItems[list.currentIndex-1]) {
var el = listItems[list.currentIndex-1];
var filterDropdownEl = el.closest('.filter-dropdown');
el.classList.add('dropdown-active');
if (filterDropdownEl) {
var filterDropdownBottom = filterDropdownEl.offsetHeight;
var elOffsetTop = el.offsetTop - 30;
if (elOffsetTop > filterDropdownBottom) {
filterDropdownEl.scrollTop = elOffsetTop - filterDropdownBottom;
}
}
}
listItems[currentIndex-1].classList.add('dropdown-active');
}
};
......@@ -597,13 +625,13 @@ require('./window')(function(w){
var list = e.detail.hook.list;
removeHighlight(list);
list.show();
currentIndex = 0;
list.currentIndex = 0;
isUpArrow = false;
isDownArrow = false;
};
var selectItem = function selectItem(list) {
var listItems = removeHighlight(list);
var currentItem = listItems[currentIndex-1];
var currentItem = listItems[list.currentIndex-1];
var listEvent = new CustomEvent('click.dl', {
detail: {
list: list,
......@@ -617,6 +645,8 @@ require('./window')(function(w){
var keydown = function keydown(e){
var typedOn = e.target;
var list = e.detail.hook.list;
var currentIndex = list.currentIndex;
isUpArrow = false;
isDownArrow = false;
......@@ -648,6 +678,7 @@ require('./window')(function(w){
if(isUpArrow){ currentIndex--; }
if(isDownArrow){ currentIndex++; }
if(currentIndex < 0){ currentIndex = 0; }
list.currentIndex = currentIndex;
setMenuForArrows(e.detail.hook.list);
};
......
......@@ -29,6 +29,7 @@ require('../window')(function(w){
init: function init(hook) {
var self = this;
var config = hook.config.droplabAjax;
this.hook = hook;
if (!config || !config.endpoint || !config.method) {
return;
......@@ -52,19 +53,26 @@ require('../window')(function(w){
this._loadUrlData(config.endpoint)
.then(function(d) {
if (config.loadingTemplate) {
var dataLoadingTemplate = hook.list.list.querySelector('[data-loading-template]');
var dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]');
if (dataLoadingTemplate) {
dataLoadingTemplate.outerHTML = self.listTemplate;
}
}
hook.list[config.method].call(hook.list, d);
if (!self.hook.list.hidden) {
self.hook.list[config.method].call(self.hook.list, d);
}
}).catch(function(e) {
throw new droplabAjaxException(e.message || e);
});
},
destroy: function() {
if (this.listTemplate) {
var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
dynamicList.outerHTML = this.listTemplate;
}
}
};
});
......@@ -76,4 +84,4 @@ module.exports = function(callback) {
};
},{}]},{},[1])(1)
});
\ No newline at end of file
});
......@@ -93,6 +93,7 @@ require('../window')(function(w){
self.hook.list.setData.call(self.hook.list, data);
}
self.notLoading();
self.hook.list.currentIndex = 0;
});
},
......@@ -142,4 +143,4 @@ module.exports = function(callback) {
};
},{}]},{},[1])(1)
});
\ No newline at end of file
});
......@@ -6,6 +6,8 @@ require('../window')(function(w){
w.droplabFilter = {
keydownWrapper: function(e){
var hiddenCount = 0;
var dataHiddenCount = 0;
var list = e.detail.hook.list;
var data = list.data;
var value = e.detail.hook.trigger.value.toLowerCase();
......@@ -27,10 +29,22 @@ require('../window')(function(w){
};
}
dataHiddenCount = data.filter(function(o) {
return !o.droplab_hidden;
}).length;
matches = data.map(function(o) {
return filterFunction(o, value);
});
list.render(matches);
hiddenCount = matches.filter(function(o) {
return !o.droplab_hidden;
}).length;
if (dataHiddenCount !== hiddenCount) {
list.render(matches);
list.currentIndex = 0;
}
},
init: function init(hookInput) {
......@@ -57,4 +71,4 @@ module.exports = function(callback) {
};
},{}]},{},[1])(1)
});
\ No newline at end of file
});
......@@ -3,7 +3,6 @@
//= require ./components/environment
//= require ./vue_resource_interceptor
$(() => {
window.gl = window.gl || {};
......
......@@ -84,7 +84,7 @@
let inputValue = input.value;
// Replace all spaces inside quote marks with underscores
// This helps with matching the beginning & end of a token:key
inputValue = inputValue.replace(/"(.*?)"/g, str => str.replace(/\s/g, '_'));
inputValue = inputValue.replace(/("(.*?)"|:\s+)/g, str => str.replace(/\s/g, '_'));
// Get the right position for the word selected
// Regex matches first space
......
......@@ -64,13 +64,26 @@
}
checkForEnter(e) {
if (e.keyCode === 38 || e.keyCode === 40) {
const selectionStart = this.filteredSearchInput.selectionStart;
e.preventDefault();
this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
}
if (e.keyCode === 13) {
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
const dropdownEl = dropdown.element;
const activeElements = dropdownEl.querySelectorAll('.dropdown-active');
e.preventDefault();
// Prevent droplab from opening dropdown
this.dropdownManager.destroyDroplab();
if (!activeElements.length) {
// Prevent droplab from opening dropdown
this.dropdownManager.destroyDroplab();
this.search();
this.search();
}
}
}
......
......@@ -61,7 +61,6 @@
return labels;
}
/**
* Will return only labels that were marked previously and the user has unmarked
* @return {Array} Label IDs
......@@ -80,7 +79,6 @@
return result;
}
/**
* Simple form serialization, it will return just what we need
* Returns key/value pairs from form data
......
......@@ -159,5 +159,19 @@
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
};
/**
this will take in the headers from an API response and normalize them
this way we don't run into production issues when nginx gives us lowercased header keys
*/
w.gl.utils.normalizeHeaders = (headers) => {
const upperCaseHeaders = {};
Object.keys(headers).forEach((e) => {
upperCaseHeaders[e.toUpperCase()] = headers[e];
});
return upperCaseHeaders;
};
})(window);
}).call(this);
......@@ -220,7 +220,6 @@
})(this));
};
/*
Increase @pollingInterval up to 120 seconds on every function call,
if `shouldReset` has a truthy value, 'null' or 'undefined' the variable
......@@ -244,7 +243,6 @@
return this.initRefresh();
};
Notes.prototype.handleCreateChanges = function(note) {
if (typeof note === 'undefined') {
return;
......@@ -294,7 +292,6 @@
}
};
/*
Check if note does not exists on page
*/
......@@ -307,7 +304,6 @@
return this.view === 'parallel';
};
/*
Render note in discussion area.
......@@ -358,7 +354,6 @@
return this.updateNotesCount(1);
};
/*
Called in response the main target form has been successfully submitted.
......@@ -390,7 +385,6 @@
return form.find(".js-note-text").trigger("input");
};
/*
Shows the main form and does some setup on it.
......@@ -415,7 +409,6 @@
return this.parentTimeline = form.parents('.timeline');
};
/*
General note form setup.
......@@ -432,7 +425,6 @@
return new Autosave(textarea, ["Note", form.find("#note_noteable_type").val(), form.find("#note_noteable_id").val(), form.find("#note_commit_id").val(), form.find("#note_type").val(), form.find("#note_line_code").val(), form.find("#note_position").val()]);
};
/*
Called in response to the new note form being submitted
......@@ -448,7 +440,6 @@
return new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', this.parentTimeline);
};
/*
Called in response to the new note form being submitted
......@@ -473,7 +464,6 @@
this.removeDiscussionNoteForm($form);
};
/*
Called in response to the edit note form being submitted
......@@ -498,7 +488,6 @@
}
};
Notes.prototype.checkContentToAllowEditing = function($el) {
var initialContent = $el.find('.original-note-content').text().trim();
var currentContent = $el.find('.note-textarea').val();
......@@ -522,7 +511,6 @@
return isAllowed;
};
/*
Called in response to clicking the edit note link
......@@ -551,7 +539,6 @@
this.putEditFormInPlace($target);
};
/*
Called in response to clicking the edit note link
......@@ -596,7 +583,6 @@
return form.find('.js-note-text').val(form.find('form.edit-note').data('original-note'));
};
/*
Called in response to deleting a note of any kind.
......@@ -636,7 +622,6 @@
return this.updateNotesCount(-1);
};
/*
Called in response to clicking the delete attachment link
......@@ -653,7 +638,6 @@
return note.find(".current-note-edit-form").remove();
};
/*
Called when clicking on the "reply" button for a diff line.
......@@ -673,7 +657,6 @@
return this.setupDiscussionNoteForm(replyLink, form);
};
/*
Shows the diff or discussion form and does some setup on it.
......@@ -715,7 +698,6 @@
.addClass("discussion-form js-discussion-note-form");
};
/*
Called when clicking on the "add a comment" button on the side of a diff line.
......@@ -772,7 +754,6 @@
}
};
/*
Called in response to "cancel" on a diff note form.
......@@ -806,7 +787,6 @@
return this.removeDiscussionNoteForm(form);
};
/*
Called after an attachment file has been selected.
......@@ -821,7 +801,6 @@
return form.find(".js-attachment-filename").text(filename);
};
/*
Called when the tab visibility changes
*/
......
(() => {
class VersionCheckImage {
static bindErrorEvent(imageElement) {
imageElement.off('error').on('error', () => imageElement.hide());
}
}
window.gl = window.gl || {};
gl.VersionCheckImage = VersionCheckImage;
})();
......@@ -22,47 +22,51 @@
<div class="controls pull-right">
<div class="btn-group inline">
<div class="btn-group">
<a
<button
v-if='actions'
class="dropdown-toggle btn btn-default js-pipeline-dropdown-manual-actions"
class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
data-toggle="dropdown"
title="Manual build"
alt="Manual Build"
data-placement="top"
data-toggle="dropdown"
aria-label="Manual build"
>
<span v-html='svgs.iconPlay'></span>
<i class="fa fa-caret-down"></i>
</a>
<span v-html='svgs.iconPlay' aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for='action in pipeline.details.manual_actions'>
<a
rel="nofollow"
data-method="post"
:href='action.path'
title="Manual build"
>
<span v-html='svgs.iconPlay'></span>
<span title="Manual build">{{action.name}}</span>
<span v-html='svgs.iconPlay' aria-hidden="true"></span>
<span>{{action.name}}</span>
</a>
</li>
</ul>
</div>
<div class="btn-group">
<a
<button
v-if='artifacts'
class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download"
class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
data-toggle="dropdown"
title="Artifacts"
data-placement="top"
data-toggle="dropdown"
type="button"
aria-label="Artifacts"
>
<i class="fa fa-download"></i>
<i class="fa fa-caret-down"></i>
</a>
<i class="fa fa-download" aria-hidden="true"></i>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for='artifact in pipeline.details.artifacts'>
<a
rel="nofollow"
:href='artifact.path'
>
<i class="fa fa-download"></i>
<i class="fa fa-download" aria-hidden="true"></i>
<span>{{download(artifact.name)}}</span>
</a>
</li>
......@@ -76,9 +80,12 @@
title="Retry"
rel="nofollow"
data-method="post"
data-placement="top"
data-toggle="dropdown"
:href='pipeline.retry_path'
aria-label="Retry"
>
<i class="fa fa-repeat"></i>
<i class="fa fa-repeat" aria-hidden="true"></i>
</a>
<a
v-if='pipeline.flags.cancelable'
......@@ -86,10 +93,12 @@
title="Cancel"
rel="nofollow"
data-method="post"
data-placement="top"
data-toggle="dropdown"
:href='pipeline.cancel_path'
data-original-title="Cancel"
aria-label="Cancel"
>
<i class="fa fa-remove"></i>
<i class="fa fa-remove" aria-hidden="true"></i>
</a>
</div>
</div>
......
......@@ -82,12 +82,13 @@
data-placement="top"
data-toggle="dropdown"
type="button"
:aria-label='stage.title'
>
<span v-html="svg"></span>
<i class="fa fa-caret-down "></i>
<span v-html="svg" aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div class="arrow-up"></div>
<div class="arrow-up" aria-hidden="true"></div>
<div
@click='keepGraph($event)'
:class="dropdownClass"
......
......@@ -4,19 +4,15 @@
((gl) => {
const pageValues = (headers) => {
const normalizedHeaders = {};
Object.keys(headers).forEach((e) => {
normalizedHeaders[e.toUpperCase()] = headers[e];
});
const normalized = gl.utils.normalizeHeaders(headers);
const paginationInfo = {
perPage: +normalizedHeaders['X-PER-PAGE'],
page: +normalizedHeaders['X-PAGE'],
total: +normalizedHeaders['X-TOTAL'],
totalPages: +normalizedHeaders['X-TOTAL-PAGES'],
nextPage: +normalizedHeaders['X-NEXT-PAGE'],
previousPage: +normalizedHeaders['X-PREV-PAGE'],
perPage: +normalized['X-PER-PAGE'],
page: +normalized['X-PAGE'],
total: +normalized['X-TOTAL'],
totalPages: +normalized['X-TOTAL-PAGES'],
nextPage: +normalized['X-NEXT-PAGE'],
previousPage: +normalized['X-PREV-PAGE'],
};
return paginationInfo;
......
......@@ -82,7 +82,12 @@
}
.block-controls {
float: right;
display: -webkit-flex;
display: flex;
-webkit-justify-content: flex-end;
justify-content: flex-end;
-webkit-flex: 1;
flex: 1;
.control {
float: left;
......@@ -282,3 +287,8 @@
}
}
}
.flex-container-block {
display: -webkit-flex;
display: flex;
}
......@@ -79,6 +79,16 @@
overflow: auto;
}
%filter-dropdown-item-btn-hover {
background-color: $dropdown-hover-color;
color: $white-light;
text-decoration: none;
.avatar {
border-color: $white-light;
}
}
.filter-dropdown-item {
.btn {
border: none;
......@@ -103,13 +113,7 @@
&:hover,
&:focus {
background-color: $dropdown-hover-color;
color: $white-light;
text-decoration: none;
.avatar {
border-color: $white-light;
}
@extend %filter-dropdown-item-btn-hover;
}
}
......@@ -131,6 +135,12 @@
}
}
.filter-dropdown-item.dropdown-active {
.btn {
@extend %filter-dropdown-item-btn-hover;
}
}
.hint-dropdown {
width: 250px;
}
......
......@@ -288,6 +288,10 @@
}
}
}
.tooltip {
white-space: nowrap;
}
}
.build-link {
......
......@@ -962,8 +962,32 @@ a.allowed-to-push {
.variables-table {
table-layout: fixed;
&.table-responsive {
border: none;
}
.variable-key {
width: 30%;
width: 300px;
max-width: 300px;
overflow: hidden;
word-wrap: break-word;
// override bootstrap
white-space: normal!important;
@media (max-width: $screen-sm-max) {
width: 150px;
max-width: 150px;
}
}
.variable-value {
@media(max-width: $screen-xs-max) {
width: 150px;
max-width: 150px;
overflow: hidden;
word-wrap: break-word;
}
}
}
......
......@@ -14,12 +14,8 @@ class ConfirmationsController < Devise::ConfirmationsController
if signed_in?(resource_name)
after_sign_in_path_for(resource)
else
sign_in(resource)
if signed_in?(resource_name)
after_sign_in_path_for(resource)
else
new_session_path(resource_name)
end
flash[:notice] += " Please sign in."
new_session_path(resource_name)
end
end
end
......@@ -46,6 +46,8 @@ class SearchController < ApplicationController
end
@search_objects = @search_results.objects(@scope, params[:page])
check_single_commit_result
end
def autocomplete
......@@ -60,4 +62,16 @@ class SearchController < ApplicationController
render json: search_autocomplete_opts(term).to_json
end
private
def check_single_commit_result
if @search_results.single_commit_result?
only_commit = @search_results.objects('commits').first
query = params[:search].strip.downcase
found_by_commit_sha = Commit.valid_hash?(query) && only_commit.sha.start_with?(query)
redirect_to namespace_project_commit_path(@project.namespace, @project, only_commit) if found_by_commit_sha
end
end
end
......@@ -3,7 +3,7 @@ module CompareHelper
from.present? &&
to.present? &&
from != to &&
project.feature_available?(:merge_requests, current_user) &&
can?(current_user, :create_merge_request, project) &&
project.repository.branch_names.include?(from) &&
project.repository.branch_names.include?(to)
end
......
......@@ -14,7 +14,7 @@ module GroupsHelper
def group_title(group, name = nil, url = nil)
full_title = ''
group.parents.each do |parent|
group.ancestors.each do |parent|
full_title += link_to(simple_sanitize(parent.name), group_path(parent))
full_title += ' / '.html_safe
end
......
module VersionCheckHelper
def version_status_badge
if Rails.env.production? && current_application_settings.version_check_enabled
image_tag VersionCheck.new.url
image_url = VersionCheck.new.url
image_tag image_url, class: 'js-version-status-badge'
end
end
end
......@@ -38,6 +38,14 @@ module Emails
mail_answer_thread(@snippet, note_thread_options(recipient_id))
end
def note_personal_snippet_email(recipient_id, note_id)
setup_note_mail(note_id, recipient_id)
@snippet = @note.noteable
@target_url = snippet_url(@note.noteable)
mail_answer_thread(@snippet, note_thread_options(recipient_id))
end
private
def note_target_url_options
......
......@@ -22,6 +22,17 @@ class Ability
end
end
# Given a list of users and a snippet this method returns the users that can
# read the given snippet.
def users_that_can_read_personal_snippet(users, snippet)
case snippet.visibility_level
when Snippet::INTERNAL, Snippet::PUBLIC
users
when Snippet::PRIVATE
users.include?(snippet.author) ? [snippet.author] : []
end
end
# Returns an Array of Issues that can be read by the given user.
#
# issues - The issues to reduce down to those readable by the user.
......
......@@ -55,6 +55,7 @@ class ApplicationSetting < ActiveRecord::Base
user_default_external: false
}
<<<<<<< HEAD
DEFAULTS_EE = {
elasticsearch_host: ENV['ELASTIC_HOST'] || 'localhost',
elasticsearch_port: ENV['ELASTIC_PORT'] || '9200',
......@@ -62,6 +63,9 @@ class ApplicationSetting < ActiveRecord::Base
}
DEFAULTS = DEFAULTS_CE.merge(DEFAULTS_EE)
=======
DEFAULTS = DEFAULTS_CE
>>>>>>> ce/master
serialize :restricted_visibility_levels
serialize :import_sources
......@@ -226,10 +230,13 @@ class ApplicationSetting < ActiveRecord::Base
def self.create_from_defaults
create(DEFAULTS)
<<<<<<< HEAD
end
def elasticsearch_host
read_attribute(:elasticsearch_host).split(',').map(&:strip)
=======
>>>>>>> ce/master
end
def home_page_url_column_exist
......
......@@ -21,6 +21,9 @@ class Commit
DIFF_HARD_LIMIT_FILES = 1000
DIFF_HARD_LIMIT_LINES = 50000
# The SHA can be between 7 and 40 hex characters.
COMMIT_SHA_PATTERN = '\h{7,40}'
class << self
def decorate(commits, project)
commits.map do |commit|
......@@ -52,6 +55,10 @@ class Commit
def from_hash(hash, project)
new(Gitlab::Git::Commit.new(hash), project)
end
def valid_hash?(key)
!!(/\A#{COMMIT_SHA_PATTERN}\z/ =~ key)
end
end
attr_accessor :raw
......@@ -77,8 +84,6 @@ class Commit
# Pattern used to extract commit references from text
#
# The SHA can be between 7 and 40 hex characters.
#
# This pattern supports cross-project references.
def self.reference_pattern
@reference_pattern ||= %r{
......@@ -88,7 +93,7 @@ class Commit
end
def self.link_reference_pattern
@link_reference_pattern ||= super("commit", /(?<commit>\h{7,40})/)
@link_reference_pattern ||= super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})/)
end
def to_reference(from_project = nil, full: false)
......
......@@ -51,6 +51,10 @@ module CacheMarkdownField
CACHING_CLASSES.map(&:constantize)
end
def skip_project_check?
false
end
extend ActiveSupport::Concern
included do
......@@ -112,7 +116,8 @@ module CacheMarkdownField
invalidation_method = "#{html_field}_invalidated?".to_sym
define_method(cache_method) do
html = Banzai::Renderer.cacheless_render_field(self, markdown_field)
options = { skip_project_check: skip_project_check? }
html = Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
__send__("#{html_field}=", html)
true
end
......
......@@ -49,7 +49,11 @@ module Mentionable
self.class.mentionable_attrs.each do |attr, options|
text = __send__(attr)
options = options.merge(cache_key: [self, attr], author: author)
options = options.merge(
cache_key: [self, attr],
author: author,
skip_project_check: skip_project_check?
)
extractor.analyze(text, options)
end
......@@ -121,4 +125,8 @@ module Mentionable
def cross_reference_exists?(target)
SystemNoteService.cross_reference_exists?(target, local_reference)
end
def skip_project_check?
false
end
end
......@@ -96,6 +96,11 @@ module Participable
participants.merge(ext.users)
Ability.users_that_can_read_project(participants.to_a, project)
case self
when PersonalSnippet
Ability.users_that_can_read_personal_snippet(participants.to_a, self)
else
Ability.users_that_can_read_project(participants.to_a, project)
end
end
end
......@@ -60,6 +60,21 @@ module Routable
joins(:route).where(wheres.join(' OR '))
end
end
# Builds a relation to find multiple objects that are nested under user membership
#
# Usage:
#
# Klass.member_descendants(1)
#
# Returns an ActiveRecord::Relation.
def member_descendants(user_id)
joins(:route).
joins("INNER JOIN routes r2 ON routes.path LIKE CONCAT(r2.path, '/%')
INNER JOIN members ON members.source_id = r2.source_id
AND members.source_type = r2.source_type").
where('members.user_id = ?', user_id)
end
end
private
......
......@@ -11,7 +11,7 @@ module Taskable
INCOMPLETE = 'incomplete'.freeze
ITEM_PATTERN = /
^
(?:\s*[-+*]|(?:\d+\.))? # optional list prefix
\s*(?:[-+*]|(?:\d+\.))? # optional list prefix
\s* # optional whitespace prefix
(\[\s\]|\[[xX]\]) # checkbox
(\s.+) # followed by whitespace and some text.
......
......@@ -245,7 +245,7 @@ class Group < Namespace
end
def members_with_parents
GroupMember.where(requested_at: nil, source_id: parents.map(&:id).push(id))
GroupMember.where(requested_at: nil, source_id: ancestors.map(&:id).push(id))
end
def users_with_parents
......
......@@ -916,9 +916,11 @@ class MergeRequest < ActiveRecord::Base
paths: paths
)
active_diff_notes.each do |note|
service.execute(note)
Gitlab::Timeless.timeless(note, &:save)
transaction do
active_diff_notes.each do |note|
service.execute(note)
Gitlab::Timeless.timeless(note, &:save)
end
end
end
......
......@@ -196,8 +196,26 @@ class Namespace < ActiveRecord::Base
end
end
def parents
@parents ||= parent ? parent.parents + [parent] : []
# Scopes the model on ancestors of the record
def ancestors
if parent_id
path = route.path
paths = []
until path.blank?
path = path.rpartition('/').first
paths << path
end
self.class.joins(:route).where('routes.path IN (?)', paths).reorder('routes.path ASC')
else
self.class.none
end
end
# Scopes the model on direct and indirect children of the record
def descendants
self.class.joins(:route).where('routes.path LIKE ?', "#{route.path}/%").reorder('routes.path ASC')
end
private
......
......@@ -44,7 +44,8 @@ class Note < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true
delegate :title, to: :noteable, allow_nil: true
validates :note, :project, presence: true
validates :note, presence: true
validates :project, presence: true, unless: :for_personal_snippet?
# Attachments are deprecated and are handled by Markdown uploader
validates :attachment, file_size: { maximum: :max_attachment_size }
......@@ -54,7 +55,7 @@ class Note < ActiveRecord::Base
validates :commit_id, presence: true, if: :for_commit?
validates :author, presence: true
validate unless: [:for_commit?, :importing?] do |note|
validate unless: [:for_commit?, :importing?, :for_personal_snippet?] do |note|
unless note.noteable.try(:project) == note.project
errors.add(:invalid_project, 'Note and noteable project mismatch')
end
......@@ -85,7 +86,7 @@ class Note < ActiveRecord::Base
after_initialize :ensure_discussion_id
before_validation :nullify_blank_type, :nullify_blank_line_code
before_validation :set_discussion_id
after_save :keep_around_commit
after_save :keep_around_commit, unless: :for_personal_snippet?
class << self
def model_name
......@@ -171,6 +172,14 @@ class Note < ActiveRecord::Base
noteable_type == "Snippet"
end
def for_personal_snippet?
noteable.is_a?(PersonalSnippet)
end
def skip_project_check?
for_personal_snippet?
end
# override to return commits, which are not active record
def noteable
if for_commit?
......@@ -226,6 +235,10 @@ class Note < ActiveRecord::Base
note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
end
def to_ability_name
for_personal_snippet? ? 'personal_snippet' : noteable_type.underscore
end
private
def keep_around_commit
......
......@@ -8,15 +8,16 @@ class Route < ActiveRecord::Base
presence: true,
uniqueness: { case_sensitive: false }
after_update :rename_children, if: :path_changed?
after_update :rename_descendants, if: :path_changed?
def rename_children
def rename_descendants
# We update each row separately because MySQL does not have regexp_replace.
# rubocop:disable Rails/FindEach
Route.where('path LIKE ?', "#{path_was}/%").each do |route|
# Note that update column skips validation and callbacks.
# We need this to avoid recursive call of rename_children method
# We need this to avoid recursive call of rename_descendants method
route.update_column(:path, route.path.sub(path_was, path))
end
# rubocop:enable Rails/FindEach
end
end
......@@ -192,8 +192,8 @@ class User < ActiveRecord::Base
joins(:identities).where(identities: { provider: provider })
end
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
scope :order_recent_sign_in, -> { reorder(last_sign_in_at: :desc) }
scope :order_oldest_sign_in, -> { reorder(last_sign_in_at: :asc) }
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'ASC')) }
def self.with_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
......@@ -461,6 +461,15 @@ class User < ActiveRecord::Base
Group.where("namespaces.id IN (#{union.to_sql})")
end
def nested_groups
Group.member_descendants(id)
end
def nested_projects
Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL').
member_descendants(id)
end
def refresh_authorized_projects
Users::RefreshAuthorizedProjectsService.new(self).execute
end
......
......@@ -3,9 +3,10 @@ module Notes
def execute
merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha)
note = project.notes.new(params)
note.author = current_user
note.system = false
note = Note.new(params)
note.project = project
note.author = current_user
note.system = false
if note.award_emoji?
noteable = note.noteable
......
......@@ -10,6 +10,9 @@ module Notes
# Skip system notes, like status changes and cross-references and awards
unless @note.system?
EventCreateService.new.leave_note(@note, @note.author)
return if @note.for_personal_snippet?
@note.create_cross_references!
execute_note_hooks
end
......
......@@ -12,7 +12,7 @@ module Notes
def self.supported?(note, current_user)
noteable_update_service(note) &&
current_user &&
current_user.can?(:"update_#{note.noteable_type.underscore}", note.noteable)
current_user.can?(:"update_#{note.to_ability_name}", note.noteable)
end
def supported?(note)
......
......@@ -195,8 +195,15 @@ class NotificationService
recipients = []
mentioned_users = note.mentioned_users
ability, subject = if note.for_personal_snippet?
[:read_personal_snippet, note.noteable]
else
[:read_project, note.project]
end
mentioned_users.select! do |user|
user.can?(:read_project, note.project)
user.can?(ability, subject)
end
# Add all users participating in the thread (author, assignee, comment authors)
......@@ -209,11 +216,13 @@ class NotificationService
recipients = recipients.concat(participants)
# Merge project watchers
recipients = add_project_watchers(recipients, note.project)
unless note.for_personal_snippet?
# Merge project watchers
recipients = add_project_watchers(recipients, note.project)
# Merge project with custom notification
recipients = add_custom_notifications(recipients, note.project, :new_note)
# Merge project with custom notification
recipients = add_custom_notifications(recipients, note.project, :new_note)
end
# Reject users with Mention notification level, except those mentioned in _this_ note.
recipients = reject_mention_users(recipients - mentioned_users, note.project)
......@@ -228,8 +237,7 @@ class NotificationService
recipients.delete(note.author)
recipients = recipients.uniq
# build notify method like 'note_commit_email'
notify_method = "note_#{note.noteable_type.underscore}_email".to_sym
notify_method = "note_#{note.to_ability_name}_email".to_sym
recipients.each do |recipient|
mailer.send(notify_method, recipient.id, note.id).deliver_later
......
......@@ -118,7 +118,8 @@ module Users
user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"),
user.groups_projects.select_for_project_authorization,
user.projects.select_for_project_authorization,
user.groups.joins(:shared_projects).select_for_project_authorization
user.groups.joins(:shared_projects).select_for_project_authorization,
user.nested_projects.select_for_project_authorization
]
Gitlab::SQL::Union.new(relations)
......
......@@ -4,7 +4,7 @@
- if @broadcast_message.message.present?
= render_broadcast_message(@broadcast_message)
- else
= "Your message here"
Your message here
= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f|
= form_errors(@broadcast_message)
......
%tr
%td
= "#{Gitlab::OAuth::Provider.label_for(identity.provider)} (#{identity.provider})"
#{Gitlab::OAuth::Provider.label_for(identity.provider)} (#{identity.provider})
%td
= identity.extern_uid
%td
......
......@@ -11,7 +11,7 @@
that for future communication.
%br
Registration token is
%code{ id: 'runners-token' } #{current_application_settings.runners_registration_token}
%code#runners-token= current_application_settings.runners_registration_token
.bs-callout.clearfix
.pull-left
......
......@@ -100,7 +100,7 @@
%td.build-link
- if project
= link_to ci_status_path(build.pipeline) do
%strong #{build.pipeline.short_sha}
%strong= build.pipeline.short_sha
%td.timestamp
- if build.finished_at
......
......@@ -10,7 +10,7 @@
%h4 CPU
.data
- if @cpus
%h1= "#{@cpus.length} cores"
%h1 #{@cpus.length} cores
- else
= icon('warning', class: 'text-warning')
Unable to collect CPU info
......@@ -19,7 +19,7 @@
%h4 Memory
.data
- if @memory
%h1= "#{number_to_human_size(@memory.active_bytes)} / #{number_to_human_size(@memory.total_bytes)}"
%h1 #{number_to_human_size(@memory.active_bytes)} / #{number_to_human_size(@memory.total_bytes)}
- else
= icon('warning', class: 'text-warning')
Unable to collect memory info
......@@ -28,6 +28,6 @@
%h4 Disks
.data
- @disks.each do |disk|
%h1= "#{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])}"
%p= "#{disk[:disk_name]}"
%p= "#{disk[:mount_path]}"
%h1 #{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])}
%p= disk[:disk_name]
%p= disk[:mount_path]
......@@ -186,6 +186,6 @@
- if @user.solo_owned_groups.present?
%p
This user is currently an owner in these groups:
%strong #{@user.solo_owned_groups.map(&:name).join(', ')}
%strong= @user.solo_owned_groups.map(&:name).join(', ')
%p
You must transfer ownership or delete these groups before you can delete this user.
......@@ -13,7 +13,7 @@
.file-holder
.file-title.clearfix
Content of .gitlab-ci.yml
#ci-editor.ci-editor #{@content}
#ci-editor.ci-editor= @content
= text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true)
.col-sm-12
.pull-left.prepend-top-10
......
- expanded = discussion.expanded?
%li.note.note-discussion.timeline-entry
.timeline-entry-inner
.timeline-icon
= link_to user_path(discussion.author) do
= image_tag avatar_icon(discussion.author), class: "avatar s40"
.timeline-content
.discussion.js-toggle-container{ class: discussion.id, data: { discussion_id: discussion.id } }
.discussion-header
......
......@@ -6,7 +6,7 @@
.content{ class: ('hide' unless discussion_left.expanded?) }
= render "discussions/notes", discussion: discussion_left, line_type: 'old'
- else
%td.notes_line.old= ""
%td.notes_line.old= ("")
%td.notes_content.parallel.old
.content
......@@ -16,6 +16,6 @@
.content{ class: ('hide' unless discussion_right.expanded?) }
= render "discussions/notes", discussion: discussion_right, line_type: 'new'
- else
%td.notes_line.new= ""
%td.notes_line.new= ("")
%td.notes_content.parallel.new
.content
......@@ -10,7 +10,7 @@
%p
= icon("exclamation-triangle fw")
You are an admin, which means granting access to
%strong #{@pre_auth.client.name}
%strong= @pre_auth.client.name
will allow them to interact with GitLab as an admin as well. Proceed with caution.
- if @pre_auth.scopes
......
......@@ -27,7 +27,7 @@
.panel.panel-default
.panel-heading
Users with access to
%strong #{@group.name}
%strong= @group.name
%span.badge= @members.total_count
%ul.content-list
= render partial: 'shared/members/member', collection: @members, as: :member
......
......@@ -18,7 +18,7 @@
.row-content-block.second-block
Only issues from the
%strong #{@group.name}
%strong= @group.name
group are listed here.
- if current_user
To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page.
......
......@@ -10,7 +10,7 @@
.row-content-block.second-block
Only merge requests from
%strong #{@group.name}
%strong= @group.name
group are listed here.
- if current_user
To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page.
......
......@@ -10,7 +10,7 @@
.row-content-block
Only milestones from
%strong #{@group.name}
%strong= @group.name
group are listed here.
.milestones
......
......@@ -16,7 +16,7 @@
%colgroup.import-jobs-status-col
%thead
%tr
%th= "From #{provider_title}"
%th From #{provider_title}
%th To GitLab
%th Status
%tbody
......
......@@ -37,7 +37,7 @@
%tbody
- @user_map.each do |id, user|
%tr
%td= id
%td= (id)
%td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control'
%td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control'
%td
......
......@@ -50,7 +50,7 @@
%td
= repo.name
%td.import-target
= "#{current_user.username}/#{repo.name}"
#{current_user.username}/#{repo.name}
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
Import
......
......@@ -55,7 +55,7 @@
%td
= link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank"
%td.import-target
= "#{current_user.username}/#{repo.name}"
#{current_user.username}/#{repo.name}
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
Import
......
......@@ -2,9 +2,9 @@
Assignee changed
- if @previous_assignee
from
%strong #{@previous_assignee.name}
%strong= @previous_assignee.name
to
- if issuable.assignee_id
%strong #{issuable.assignee_name}
%strong= issuable.assignee_name
- else
%strong Unassigned
%p
= "Issue was closed by #{@updated_by.name}"
Issue was closed by #{@updated_by.name}
= "Issue was closed by #{@updated_by.name}"
Issue was closed by #{@updated_by.name}
Issue ##{@issue.iid}: #{namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)}
%p
= "Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}"
Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}
= "Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}"
Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}
Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)}
......
%p
= "Issue was #{@issue_status} by #{@updated_by.name}"
Issue was #{@issue_status} by #{@updated_by.name}
%p
= "Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}"
Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}
= "Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}"
Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}
Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)}
......
%p
= "Merge Request #{@merge_request.to_reference} was merged"
Merge Request #{@merge_request.to_reference} was merged
= "Merge Request #{@merge_request.to_reference} was merged"
Merge Request #{@merge_request.to_reference} was merged
Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)}
......
New comment for Snippet <%= @snippet.id %>
<%= url_for(snippet_url(@snippet, anchor: "note_#{@note.id}")) %>
Author: <%= @note.author_name %>
<%= @note.note %>
......@@ -139,7 +139,7 @@
had
= failed.size
failed
= "#{'build'.pluralize(failed.size)}."
#{'build'.pluralize(failed.size)}.
%tr.warning
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" }
Logs may contain sensitive data. Please consider before forwarding this email.
......
......@@ -138,9 +138,9 @@
%a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
= "\##{@pipeline.id}"
successfully completed
= "#{build_count} #{'build'.pluralize(build_count)}"
#{build_count} #{'build'.pluralize(build_count)}
in
= "#{stage_count} #{'stage'.pluralize(stage_count)}."
#{stage_count} #{'stage'.pluralize(stage_count)}.
%tr.footer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
%img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/
......
= "Project #{@project.name} couldn't be exported."
Project #{@project.name} couldn't be exported.
= "The errors we encountered were:"
The errors we encountered were:
- @errors.each do |error|
#{error}
......@@ -17,7 +17,7 @@
%ul
- @message.commits.each do |commit|
%li
%strong #{link_to(commit.short_id, namespace_project_commit_url(@message.project_namespace, @message.project, commit))}
%strong= link_to(commit.short_id, namespace_project_commit_url(@message.project_namespace, @message.project, commit))
%div
%span by #{commit.author_name}
%i at #{commit.committed_date.to_s(:iso8601)}
......
......@@ -102,7 +102,7 @@
= f.text_field :username, required: true, class: 'form-control'
.help-block
Current path:
= "#{root_url}#{current_user.username}"
#{root_url}#{current_user.username}
.prepend-top-default
= f.button class: "btn btn-warning", type: "submit" do
= icon "spinner spin", class: "hidden loading-username"
......@@ -128,7 +128,7 @@
- if @user.solo_owned_groups.present?
%p
Your account is currently an owner in these groups:
%strong #{@user.solo_owned_groups.map(&:name).join(', ')}
%strong= @user.solo_owned_groups.map(&:name).join(', ')
%p
You must transfer ownership or delete these groups before you can delete your account.
.append-bottom-default
......@@ -62,7 +62,7 @@
%span.help-block
Please click the link in the confirmation email before continuing. It was sent to
= succeed "." do
%strong #{@user.unconfirmed_email}
%strong= @user.unconfirmed_email
%p
= link_to "Resend confirmation e-mail", user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post
......
......@@ -34,7 +34,7 @@
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
.file-editor.code
%pre.js-edit-mode-pane#editor #{params[:content] || local_assigns[:blob_data]}
%pre.js-edit-mode-pane#editor= params[:content] || local_assigns[:blob_data]
- if local_assigns[:path]
.js-edit-mode-pane#preview.hide
.center
......
.file-content.image_file
- if blob.svg?
- if blob.size_within_svg_limits?
- # We need to scrub SVG but we cannot do so in the RawController: it would
- # be wrong/strange if RawController modified the data.
-# We need to scrub SVG but we cannot do so in the RawController: it would
-# be wrong/strange if RawController modified the data.
- blob.load_all_data!(@repository)
- blob = sanitize_svg(blob)
%img{ src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}", alt: "#{blob.name}" }
......
......@@ -3,7 +3,7 @@
.modal-content
.modal-header
%a.close{ href: "#", "data-dismiss" => "modal" } ×
%h3.page-title #{title}
%h3.page-title= title
.modal-body
= form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal' do
.dropzone
......
......@@ -85,7 +85,7 @@
- if build.finished_at
%p.finished-at
= icon("calendar")
%span #{time_ago_with_tooltip(build.finished_at)}
%span= time_ago_with_tooltip(build.finished_at)
%td.coverage
- if coverage && build.try(:coverage)
......
......@@ -78,7 +78,7 @@
.btn-group.inline
- if actions.any?
.btn-group
%button.dropdown-toggle.btn.btn-default.js-pipeline-dropdown-manual-actions{ type: 'button', 'data-toggle' => 'dropdown' }
%button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual build', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label': 'Manual build' }
= custom_icon('icon_play')
= icon('caret-down', 'aria-hidden' => 'true')
%ul.dropdown-menu.dropdown-menu-align-right
......@@ -89,7 +89,7 @@
%span= build.name
- if artifacts.present?
.btn-group
%button.dropdown-toggle.btn.btn-default.build-artifacts.js-pipeline-dropdown-download{ type: 'button', 'data-toggle' => 'dropdown' }
%button.dropdown-toggle.btn.btn-default.build-artifacts.has-tooltip.js-pipeline-dropdown-download{ type: 'button', title: 'Artifacts', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label': 'Artifacts' }
= icon("download")
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
......@@ -102,8 +102,8 @@
- if can?(current_user, :update_pipeline, pipeline.project)
.cancel-retry-btns.inline
- if pipeline.retryable?
= link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: "Retry", method: :post do
= link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: 'Retry', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label': 'Retry' , method: :post do
= icon("repeat")
- if pipeline.cancelable?
= link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do
= link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: 'Cancel', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label': 'Cancel' , method: :post do
= icon("remove")
......@@ -2,7 +2,7 @@
- commits, hidden = limited_commits(@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
%ul.content-list.commit-list.table-list.table-wide
= render commits, project: project, ref: ref
......
......@@ -9,10 +9,13 @@
= render "head"
%div{ class: container_class }
.row-content-block.second-block.content-component-block
.row-content-block.second-block.content-component-block.flex-container-block
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'commits'
%ul.breadcrumb.repo-breadcrumb
= commits_breadcrumbs
.block-controls.hidden-xs.hidden-sm
- if @merge_request.present?
.control
......@@ -30,8 +33,6 @@
.control
= link_to namespace_project_commits_path(@project.namespace, @project, @ref, { format: :atom, private_token: current_user.private_token }), title: "Commits Feed", class: 'btn' do
= icon("rss")
%ul.breadcrumb.repo-breadcrumb
= commits_breadcrumbs
= render 'projects/commits/mirror_status'
......
......@@ -16,9 +16,9 @@
There isn't anything to compare.
%p.slead
- if params[:to] == params[:from]
%span.label-branch #{params[:from]}
%span.label-branch= params[:from]
and
%span.label-branch #{params[:to]}
%span.label-branch= params[:to]
are the same.
- else
You'll need to use different branch names to get a valid comparison.
%tr.deployment
%td
%strong= "##{deployment.iid}"
%strong ##{deployment.iid}
%td
= render 'projects/deployments/commit', deployment: deployment
......@@ -8,7 +8,7 @@
%td.build-column
- if deployment.deployable
= link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do
= "#{deployment.deployable.name} (##{deployment.deployable.id})"
#{deployment.deployable.name} (##{deployment.deployable.id})
- if deployment.user
by
= user_avatar(user: deployment.user, size: 20)
......
.diff-content.diff-wrap-lines
- # Skip all non non-supported blobs
-# Skip all non non-supported blobs
- return unless blob.respond_to?(:text?)
- if diff_file.too_large?
.nothing-here-block This diff could not be displayed because it is too large.
......
......@@ -25,4 +25,4 @@
- if diff_file.mode_changed?
%small
= "#{diff_file.a_mode}#{diff_file.b_mode}"
#{diff_file.a_mode}#{diff_file.b_mode}
......@@ -9,7 +9,7 @@
%span.wrap
.frame{ class: image_diff_class(diff) }
%img{ src: diff.deleted_file ? old_file_raw_path : file_raw_path, alt: diff.new_path }
%p.image-info= "#{number_to_human_size file.size}"
%p.image-info= number_to_human_size(file.size)
- else
.image
.two-up.view
......@@ -18,7 +18,7 @@
%a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path)) }
%img{ src: old_file_raw_path, alt: diff.old_path }
%p.image-info.hide
%span.meta-filesize= "#{number_to_human_size old_file.size}"
%span.meta-filesize= number_to_human_size(old_file.size)
|
%b W:
%span.meta-width
......@@ -30,7 +30,7 @@
%a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.new_ref, diff.new_path)) }
%img{ src: file_raw_path, alt: diff.new_path }
%p.image-info.hide
%span.meta-filesize= "#{number_to_human_size file.size}"
%span.meta-filesize= number_to_human_size(file.size)
|
%b W:
%span.meta-width
......
......@@ -2,7 +2,7 @@
.commit-stat-summary
Showing
= link_to '#', class: 'js-toggle-button' do
%strong #{pluralize(diff_files.size, "changed file")}
%strong= pluralize(diff_files.size, "changed file")
with
%strong.cgreen #{diff_files.sum(&:added_lines)} additions
and
......
.top-area
.nav-text
- full_count_title = "#{@public_forks_count} public and #{@private_forks_count} private"
= "#{pluralize(@total_forks_count, 'fork')}: #{full_count_title}"
#{pluralize(@total_forks_count, 'fork')}: #{full_count_title}
.nav-controls
= form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
......
......@@ -77,7 +77,7 @@
- if generic_commit_status.finished_at
%p.finished-at
= icon("calendar")
%span #{time_ago_with_tooltip(generic_commit_status.finished_at)}
%span= time_ago_with_tooltip(generic_commit_status.finished_at)
%td.coverage
- if coverage && generic_commit_status.try(:coverage)
......
......@@ -11,7 +11,7 @@
%p.lead
Commit statistics for
%strong #{@ref}
%strong= @ref
#{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')}
.row
......@@ -19,19 +19,19 @@
%ul
%li
%p.lead
%strong #{@commits_graph.commits.size}
%strong= @commits_graph.commits.size
commits during
%strong #{@commits_graph.duration}
%strong= @commits_graph.duration
days
%li
%p.lead
Average
%strong #{@commits_graph.commit_per_day}
%strong= @commits_graph.commit_per_day
commits per day
%li
%p.lead
Contributed by
%strong #{@commits_graph.authors}
%strong= @commits_graph.authors
authors
.col-md-6
%div
......
......@@ -3,9 +3,9 @@
%p.slead
- source_title, target_title = format_mr_branch_names(@merge_request)
From
%strong.label-branch #{source_title}
%strong.label-branch= source_title
%span into
%strong.label-branch #{target_title}
%strong.label-branch= target_title
%span.pull-right
= link_to 'Change branches', mr_change_branches_path(@merge_request)
......@@ -43,7 +43,7 @@
#commits.commits.tab-pane.active
= render "projects/merge_requests/show/commits"
#diffs.diffs.tab-pane
- # This tab is always loaded via AJAX
-# This tab is always loaded via AJAX
- if @pipelines.any?
#pipelines.pipelines.tab-pane
= render "projects/merge_requests/show/pipelines"
......
......@@ -92,11 +92,11 @@
= render "projects/merge_requests/discussion"
#commits.commits.tab-pane
- # This tab is always loaded via AJAX
-# This tab is always loaded via AJAX
#pipelines.pipelines.tab-pane
- # This tab is always loaded via AJAX
-# This tab is always loaded via AJAX
#diffs.diffs.tab-pane
- # This tab is always loaded via AJAX
-# This tab is always loaded via AJAX
.mr-loading-status
= spinner
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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