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
......
......@@ -25,7 +25,7 @@
latest version
- else
version #{version_index(merge_request_diff)}
.monospace #{short_sha(merge_request_diff.head_commit_sha)}
.monospace= short_sha(merge_request_diff.head_commit_sha)
%small
#{number_with_delimiter(merge_request_diff.commits_count)} #{'commit'.pluralize(merge_request_diff.commits_count)},
= time_ago_with_tooltip(merge_request_diff.created_at)
......@@ -55,14 +55,14 @@
latest version
- else
version #{version_index(merge_request_diff)}
.monospace #{short_sha(merge_request_diff.head_commit_sha)}
.monospace= short_sha(merge_request_diff.head_commit_sha)
%small
= time_ago_with_tooltip(merge_request_diff.created_at)
%li
= link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do
%strong
#{@merge_request.target_branch} (base)
.monospace #{short_sha(@merge_request_diff.base_commit_sha)}
.monospace= short_sha(@merge_request_diff.base_commit_sha)
- if different_base?(@start_version, @merge_request_diff)
.content-block
......@@ -72,7 +72,7 @@
= link_to namespace_project_compare_path(@project.namespace, @project, from: @start_version.base_commit_sha, to: @merge_request_diff.base_commit_sha) do
new commits
from
%code #{@merge_request.target_branch}
%code= @merge_request.target_branch
- unless @merge_request_diff.latest? && !@start_sha
.comments-disabled-notif.content-block
......
......@@ -13,8 +13,8 @@
%span.ci-coverage
- elsif @merge_request.has_ci?
- # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX
- # TODO, remove in later versions when services like Jenkins will set CI status via Commit status API
-# Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX
-# TODO, remove in later versions when services like Jenkins will set CI status via Commit status API
.mr-widget-heading
- %w[success skipped canceled failed running pending].each do |status|
.ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: "display:none" }
......
......@@ -69,7 +69,7 @@
- if note_editable
.original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
#{note.note}
%textarea.hidden.js-task-list-field.original-task-list #{note.note}
%textarea.hidden.js-task-list-field.original-task-list= note.note
.note-awards
= render 'award_emoji/awards_block', awardable: note, inline: false
- if note.system
......
.panel.panel-default
.panel-heading
Group members with access to
%strong #{@group.name}
%strong= @group.name
%span.badge= members.size
- if can?(current_user, :admin_group_member, @group)
.controls
......
.panel.panel-default.project-members-groups
.panel-heading
Groups with access to
%strong #{@project.name}
%strong= @project.name
%span.badge= group_links.size
%ul.content-list
= render partial: 'shared/members/group', collection: group_links, as: :group_link
......@@ -5,9 +5,9 @@
.panel.panel-default
.panel-heading
Shared with
%strong #{shared_group.name}
%strong= shared_group.name
group, members with
%strong #{group_links.human_access}
%strong= group_links.human_access
role (#{shared_group_users_count})
- if can?(current_user, :admin_group, shared_group)
.panel-head-actions
......
.panel.panel-default
.panel-heading
Members with access to
%strong #{@project.name}
%strong= @project.name
%span.badge= @project_members.total_count
= form_tag namespace_project_settings_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
.form-group
......
......@@ -7,7 +7,7 @@
.oneline
.title
Release notes for tag
%strong #{@tag.name}
%strong= @tag.name
= form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f|
......
......@@ -9,10 +9,10 @@
(checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} for information on how to install it).
%li
Specify the following URL during the Runner setup:
%code #{ci_root_url(only_path: false)}
%code= ci_root_url(only_path: false)
%li
Use the following registration token during setup:
%code #{@project.runners_token}
%code= @project.runners_token
%li
Start the Runner!
......
......@@ -4,4 +4,4 @@
.col-sm-9.col-sm-offset-3
= link_to new_namespace_project_mattermost_path(@project.namespace, @project), class: 'btn btn-lg' do
= custom_icon('mattermost_logo', size: 15)
= 'Add to Mattermost'
Add to Mattermost
......@@ -3,4 +3,4 @@
%h4
= icon('search')
We couldn't find any results matching
%code #{@search_term}
%code= @search_term
......@@ -2,7 +2,7 @@
%h4
= link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do
%span.term.str-truncated= merge_request.title
.pull-right #{merge_request.to_reference}
.pull-right= merge_request.to_reference
- if merge_request.description.present?
.description.term
= preserve do
......
......@@ -9,7 +9,7 @@
= link_to user_snippets_path(snippet.author) do
= image_tag avatar_icon(snippet.author_email), class: "avatar avatar-inline s16", alt: ''
= snippet.author_name
%span.light #{time_ago_with_tooltip(snippet.created_at)}
%span.light= time_ago_with_tooltip(snippet.created_at)
%h4.snippet-title
- snippet_path = reliable_snippet_path(snippet)
= link_to snippet_path do
......
......@@ -20,4 +20,4 @@
= link_to user_snippets_path(snippet_title.author) do
= image_tag avatar_icon(snippet_title.author_email), class: "avatar avatar-inline s16", alt: ''
= snippet_title.author_name
%span.light #{time_ago_with_tooltip(snippet_title.created_at)}
%span.light= time_ago_with_tooltip(snippet_title.created_at)
......@@ -15,7 +15,7 @@
To prevent accidental actions we ask you to confirm your intention.
%br
Please type
%code.js-confirm-danger-match #{phrase}
%code.js-confirm-danger-match= phrase
to proceed or close this modal to cancel.
.form-group
......
......@@ -6,14 +6,14 @@
= link_to milestones_filter_path(state: 'opened') do
Open
- if @project
%span.badge #{counts[:opened]}
%span.badge= counts[:opened]
%li{ class: milestone_class_for_state(params[:state], 'closed') }>
= link_to milestones_filter_path(state: 'closed') do
Closed
- if @project
%span.badge #{counts[:closed]}
%span.badge= counts[:closed]
%li{ class: milestone_class_for_state(params[:state], 'all') }>
= link_to milestones_filter_path(state: 'all') do
All
- if @project
%span.badge #{counts[:all]}
%span.badge= counts[:all]
......@@ -32,7 +32,7 @@
- if group_member
as
%span #{group_member.human_access}
%span= group_member.human_access
- if group.description.present?
.description
......
......@@ -70,7 +70,7 @@
- if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project))
.inline.prepend-left-10
Please review the
%strong #{link_to 'contribution guidelines', guide_url}
%strong= link_to('contribution guidelines', guide_url)
for this project.
- if issuable.new_record?
......
......@@ -5,5 +5,5 @@
- scopes.each do |scope|
%fieldset
= check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}"
= label_tag "#{prefix}_scopes_#{scope}", scope
%span= "(#{t(scope, scope: [:doorkeeper, :scopes])})"
= label_tag ("#{prefix}_scopes_#{scope}"), scope
%span= t(scope, scope: [:doorkeeper, :scopes])
%h4.prepend-top-20
Contributions for
%strong #{@calendar_date.to_s(:short)}
%strong= @calendar_date.to_s(:short)
- if @events.any?
%ul.bordered-list
......
......@@ -110,16 +110,16 @@
= spinner
#groups.tab-pane
- # This tab is always loaded via AJAX
-# This tab is always loaded via AJAX
#contributed.tab-pane
- # This tab is always loaded via AJAX
-# This tab is always loaded via AJAX
#projects.tab-pane
- # This tab is always loaded via AJAX
-# This tab is always loaded via AJAX
#snippets.tab-pane
- # This tab is always loaded via AJAX
-# This tab is always loaded via AJAX
.loading-status
= spinner
......
---
title: 'Allows to search within project by commit hash'
merge_request:
author: YarNayar
---
title: Fix nested tasks in ordered list
merge_request: 8626
author:
---
title: Improve button accessibility on pipelines page
merge_request: 8561
author:
---
title: Fix tab index order on branch commits list page
merge_request:
author: Ryan Harris
---
title: Fix Sort by Recent Sign-in in Admin Area
merge_request: 8637
author: Poornima M
---
title: Hide version check image if there is no internet connection
merge_request: 8355
author: Ken Ding
---
title: Disable automatic login after clicking email confirmation links
merge_request: 7472
author:
---
title: Make MR-review-discussions more reliable
merge_request:
author:
---
title: adds avatar for discussion note
merge_request: 8734
author:
---
title: Flag multiple empty lines in eslint, fix offenses.
merge_request: 8137
author:
---
title: Support notes when a project is not specified (personal snippet notes)
merge_request: 8468
author:
---
title: 'Search feature: redirects to commit page if query is commit sha and only commit
found'
merge_request: 8028
author: YarNayar
---
title: Only show Merge Request button when user can create a MR
merge_request: 8639
author:
---
title: Requeue pending deletion projects
merge_request:
author:
......@@ -107,6 +107,7 @@ def instrument_classes(instrumentation)
instrumentation.instrument_methods(Gitlab::Highlight)
instrumentation.instrument_instance_methods(Gitlab::Highlight)
<<<<<<< HEAD
instrumentation.instrument_methods(Elasticsearch::Git::Repository)
instrumentation.instrument_instance_methods(Elasticsearch::Git::Repository)
......@@ -129,6 +130,8 @@ def instrument_classes(instrumentation)
instrumentation.instrument_instance_methods(Elastic::SnippetsSearch)
instrumentation.instrument_instance_methods(Elastic::WikiRepositoriesSearch)
=======
>>>>>>> ce/master
# This is a Rails scope so we have to instrument it manually.
instrumentation.instrument_method(Project, :visible_to_user)
end
......
......@@ -48,13 +48,11 @@ end
scope(path: 'groups/*id',
controller: :groups,
constraints: { id: Gitlab::Regex.namespace_route_regex }) do
constraints: { id: Gitlab::Regex.namespace_route_regex, format: /(html|json|atom)/ }) do
get :edit, as: :edit_group
get :issues, as: :issues_group
get :merge_requests, as: :merge_requests_group
get :projects, as: :projects_group
get :activity, as: :activity_group
get '/', action: :show, as: :group_canonical
end
# Must be last route in this file
get 'groups/*id' => 'groups#show', as: :group_canonical, constraints: { id: Gitlab::Regex.namespace_route_regex }
......@@ -3,7 +3,7 @@ class AddEstimateToIssuablesCe < ActiveRecord::Migration
DOWNTIME = false
def change
def up
unless column_exists?(:issues, :time_estimate)
add_column :issues, :time_estimate, :integer
end
......@@ -12,4 +12,14 @@ class AddEstimateToIssuablesCe < ActiveRecord::Migration
add_column :merge_requests, :time_estimate, :integer
end
end
def down
if column_exists?(:issues, :time_estimate)
remove_column :issues, :time_estimate
end
if column_exists?(:merge_requests, :time_estimate)
remove_column :merge_requests, :time_estimate
end
end
end
......@@ -3,7 +3,7 @@ class CreateTimelogsCe < ActiveRecord::Migration
DOWNTIME = false
def change
def up
unless table_exists?(:timelogs)
create_table :timelogs do |t|
t.integer :time_spent, null: false
......@@ -17,4 +17,8 @@ class CreateTimelogsCe < ActiveRecord::Migration
add_index :timelogs, :user_id
end
end
def down
drop_table :timelogs if table_exists?(:timelogs)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RequeuePendingDeleteProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
admin = User.find_by(admin: true)
return unless admin
@offset = 0
loop do
ids = pending_delete_batch
break if ids.rows.count.zero?
args = ids.map { |id| [id['id'], admin.id, {}] }
Sidekiq::Client.push_bulk('class' => "ProjectDestroyWorker", 'args' => args)
@offset += 1
end
end
def down
# noop
end
private
def pending_delete_batch
connection.exec_query(find_batch)
end
BATCH_SIZE = 5000
def find_batch
projects = Arel::Table.new(:projects)
projects.project(projects[:id]).
where(projects[:pending_delete].eq(true)).
where(projects[:namespace_id].not_eq(nil)).
skip(@offset * BATCH_SIZE).
take(BATCH_SIZE).
to_sql
end
end
......@@ -6,7 +6,7 @@
## User documentation
- [Account Security](user/account/security.md) Securing your account via two-factor authentication, etc.
- [Account Security](user/profile/account/two_factor_authentication.md) Securing your account via two-factor authentication, etc.
- [API](api/README.md) Automate GitLab via a simple and powerful API.
- [CI/CD](ci/README.md) GitLab Continuous Integration (CI) and Continuous Delivery (CD) getting started, `.gitlab-ci.yml` options, and examples.
- [Custom templates for issues and merge requests](customization/issue_and_merge_request_template.md) Pre-fill the description of issues and merge requests to your liking.
......
......@@ -27,6 +27,7 @@ Ruby Version: 2.1.5p273
Gem Version: 2.4.3
Bundler Version: 1.7.6
Rake Version: 10.3.2
Redis Version: 3.2.5
Sidekiq Version: 2.17.8
GitLab information
......
......@@ -78,7 +78,7 @@ PUT /projects/:id/environments/:environments_id
| `external_url` | string | no | The new external_url |
```bash
curl --request PUT --data "name=staging&external_url=https://staging.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1"
curl --request PUT --data "name=staging&external_url=https://staging.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments/1"
```
Example response:
......@@ -106,7 +106,7 @@ DELETE /projects/:id/environments/:environment_id
| `environment_id` | integer | yes | The ID of the environment |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1"
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments/1"
```
Example response:
......
......@@ -124,7 +124,7 @@ Download Ruby and compile it:
mkdir /tmp/ruby && cd /tmp/ruby
curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz
echo 'a8db9ce7f9110320f33b8325200e3ecfbd2b534b ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
cd ruby-2.3.3
./configure --disable-install-rdoc
make
......
# Two-factor Authentication (2FA)
Two-factor Authentication (2FA) provides an additional level of security to your
GitLab account. Once enabled, in addition to supplying your username and
password to login, you'll be prompted for a code generated by an application on
your phone.
By enabling 2FA, the only way someone other than you can log into your account
is to know your username and password *and* have access to your phone.
> **Note:**
When you enable 2FA, don't forget to back up your recovery codes. For your safety, if you
lose your codes for GitLab.com, we can't disable or recover them.
In addition to a phone application, GitLab supports U2F (universal 2nd factor) devices as
the second factor of authentication. Once enabled, in addition to supplying your username and
password to login, you'll be prompted to activate your U2F device (usually by pressing
a button on it), and it will perform secure authentication on your behalf.
> **Note:** Support for U2F devices was added in version 8.8
The U2F workflow is only supported by Google Chrome at this point, so we _strongly_ recommend
that you set up both methods of two-factor authentication, so you can still access your account
from other browsers.
> **Note:** GitLab officially only supports [Yubikey] U2F devices.
## Enabling 2FA
### Enable 2FA via mobile application
**In GitLab:**
1. Log in to your GitLab account.
1. Go to your **Profile Settings**.
1. Go to **Account**.
1. Click **Enable Two-factor Authentication**.
![Two-factor setup](2fa.png)
**On your phone:**
1. Install a compatible application. We recommend [Google Authenticator]
\(proprietary\) or [FreeOTP] \(open source\).
1. In the application, add a new entry in one of two ways:
* Scan the code with your phone's camera to add the entry automatically.
* Enter the details provided to add the entry manually.
**In GitLab:**
1. Enter the six-digit pin number from the entry on your phone into the **Pin
code** field.
1. Click **Submit**.
If the pin you entered was correct, you'll see a message indicating that
Two-Factor Authentication has been enabled, and you'll be presented with a list
of recovery codes.
### Enable 2FA via U2F device
**In GitLab:**
1. Log in to your GitLab account.
1. Go to your **Profile Settings**.
1. Go to **Account**.
1. Click **Enable Two-Factor Authentication**.
1. Plug in your U2F device.
1. Click on **Setup New U2F Device**.
1. A light will start blinking on your device. Activate it by pressing its button.
You will see a message indicating that your device was successfully set up.
Click on **Register U2F Device** to complete the process.
![Two-Factor U2F Setup](2fa_u2f_register.png)
## Recovery Codes
Should you ever lose access to your phone, you can use one of the ten provided
backup codes to login to your account. We suggest copying or printing them for
storage in a safe place. **Each code can be used only once** to log in to your
account.
If you lose the recovery codes or just want to generate new ones, you can do so
from the **Profile Settings** > **Account** page where you first enabled 2FA.
> **Note:** Recovery codes are not generated for U2F devices.
## Logging in with 2FA Enabled
Logging in with 2FA enabled is only slightly different than a normal login.
Enter your username and password credentials as you normally would, and you'll
be presented with a second prompt, depending on which type of 2FA you've enabled.
### Log in via mobile application
Enter the pin from your phone's application or a recovery code to log in.
![Two-Factor Authentication on sign in via OTP](2fa_auth.png)
### Log in via U2F device
1. Click **Login via U2F Device**
1. A light will start blinking on your device. Activate it by pressing its button.
You will see a message indicating that your device responded to the authentication request.
Click on **Authenticate via U2F Device** to complete the process.
![Two-Factor Authentication on sign in via U2F device](2fa_u2f_authenticate.png)
## Disabling 2FA
1. Log in to your GitLab account.
1. Go to your **Profile Settings**.
1. Go to **Account**.
1. Click **Disable**, under **Two-Factor Authentication**.
This will clear all your two-factor authentication registrations, including mobile
applications and U2F devices.
## Personal access tokens
When 2FA is enabled, you can no longer use your normal account password to
authenticate with Git over HTTPS on the command line, you must use a personal
access token instead.
1. Log in to your GitLab account.
1. Go to your **Profile Settings**.
1. Go to **Access Tokens**.
1. Choose a name and expiry date for the token.
1. Click on **Create Personal Access Token**.
1. Save the personal access token somewhere safe.
When using git over HTTPS on the command line, enter the personal access token
into the password field.
## Note to GitLab administrators
You need to take special care to that 2FA keeps working after
[restoring a GitLab backup](../raketasks/backup_restore.md).
[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en
[FreeOTP]: https://fedorahosted.org/freeotp/
[YubiKey]: https://www.yubico.com/products/yubikey-hardware/
This document was moved to [user/profile/account](../user/profile/account/two_factor_authentication.md).
......@@ -8,7 +8,7 @@ the [configuration](#configuration) section.
If you have a single cluster that you want to use for all your projects,
you can pre-fill the settings page with a default template. To configure the
template, see the [Services Templates](services-templates.md) document.
template, see the [Services Templates](services_templates.md) document.
## Configuration
......
......@@ -36,7 +36,7 @@ Download and compile Ruby:
```bash
mkdir /tmp/ruby && cd /tmp/ruby
curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz
echo 'a8db9ce7f9110320f33b8325200e3ecfbd2b534b ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
cd ruby-2.3.3
./configure --disable-install-rdoc
make
......
# Account Security
- [Two-Factor Authentication](two_factor_authentication.md)
This document was moved to [profile](../profile/index.md#security).
# Two-Factor Authentication
## Recovery options
If you lose your code generation device (such as your mobile phone) and you need
to disable two-factor authentication on your account, you have several options.
### Use a saved recovery code
When you enabled two-factor authentication for your account, a series of
recovery codes were generated. If you saved those codes somewhere safe, you
may use one to sign in.
First, enter your username/email and password on the GitLab sign in page. When
prompted for a two-factor code, enter one of the recovery codes you saved
previously.
> **Note:** Once a particular recovery code has been used, it cannot be used again.
You may still use the other saved recovery codes at a later time.
### Generate new recovery codes using SSH
It's not uncommon for users to forget to save the recovery codes when enabling
two-factor authentication. If you have an SSH key added to your GitLab account,
you can generate a new set of recovery codes using SSH.
Run `ssh git@gitlab.example.com 2fa_recovery_codes`. You will be prompted to
confirm that you wish to generate new codes. If you choose to continue, any
previously saved codes will be invalidated.
```bash
$ ssh git@gitlab.example.com 2fa_recovery_codes
Are you sure you want to generate new two-factor recovery codes?
Any existing recovery codes you saved will be invalidated. (yes/no)
yes
Your two-factor authentication recovery codes are:
119135e5a3ebce8e
11f6v2a498810dcd
3924c7ab2089c902
e79a3398bfe4f224
34bd7b74adbc8861
f061691d5107df1a
169bf32a18e63e7f
b510e7422e81c947
20dbed24c5e74663
df9d3b9403b9c9f0
During sign in, use one of the codes above when prompted for
your two-factor code. Then, visit your Profile Settings and add
a new device so you do not lose access to your account again.
```
Next, go to the GitLab sign in page and enter your username/email and password.
When prompted for a two-factor code, enter one of the recovery codes obtained
from the command line output.
> **Note:** After signing in, you should immediately visit your **Profile Settings
-> Account** to set up two-factor authentication with a new device.
### Ask a GitLab administrator to disable two-factor on your account
If the above two methods are not possible, you may ask a GitLab global
administrator to disable two-factor authentication for your account. Please
be aware that this will temporarily leave your account in a less secure state.
You should sign in and re-enable two-factor authentication as soon as possible
after the administrator disables it.
This document was moved to [profile/account/two_factor_authentication](../profile/account/two_factor_authentication.md).
......@@ -300,6 +300,20 @@ You can add task lists to issues, merge requests and comments. To create a task
- [x] Sub-task 2
- [ ] Sub-task 3
Tasks formatted as ordered lists are supported as well:
```no-highlight
1. [x] Completed task
1. [ ] Incomplete task
1. [ ] Sub-task 1
1. [x] Sub-task 2
```
1. [x] Completed task
1. [ ] Incomplete task
1. [ ] Sub-task 1
1. [x] Sub-task 2
Task lists can only be created in descriptions, not in titles. Task item state can be managed by editing the description's Markdown or by toggling the rendered check boxes.
### Videos
......@@ -650,7 +664,7 @@ This line is separated from the one above by two newlines, so it will be a *sepa
This line is also a separate paragraph, but...
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
This line is also a separate paragraph, and...
This line is also a separate paragraph, and...
This line is on its own line, because the previous line ends with two
spaces.
```
......@@ -662,7 +676,7 @@ This line is separated from the one above by two newlines, so it will be a *sepa
This line is also begins a separate paragraph, but...
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
This line is also a separate paragraph, and...
This line is also a separate paragraph, and...
This line is on its own line, because the previous line ends with two
spaces.
......@@ -800,4 +814,4 @@ A link starting with a `/` is relative to the wiki root.
[redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website"
[katex]: https://github.com/Khan/KaTeX "KaTeX website"
[katex-subset]: https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX "Macros supported by KaTeX"
[asciidoctor-manual]: http://asciidoctor.org/docs/user-manual/#activating-stem-support "Asciidoctor user manual"
\ No newline at end of file
[asciidoctor-manual]: http://asciidoctor.org/docs/user-manual/#activating-stem-support "Asciidoctor user manual"
......@@ -19,10 +19,12 @@ The following table depicts the various user permission levels in a project.
| Action | Guest | Reporter | Developer | Master | Owner |
|---------------------------------------|---------|------------|-------------|----------|--------|
| Create new issue | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create confidential issue | ✓ | ✓ | ✓ | ✓ | ✓ |
| View confidential issues | (✓) [^1] | ✓ | ✓ | ✓ | ✓ |
| Leave comments | ✓ | ✓ | ✓ | ✓ | ✓ |
| See a list of builds | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
| See a build log | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
| Download and browse build artifacts | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
| See a list of builds | ✓ [^2] | ✓ | ✓ | ✓ | ✓ |
| See a build log | ✓ [^2] | ✓ | ✓ | ✓ | ✓ |
| Download and browse build artifacts | ✓ [^2] | ✓ | ✓ | ✓ | ✓ |
| View wiki pages | ✓ | ✓ | ✓ | ✓ | ✓ |
| Pull project code | | ✓ | ✓ | ✓ | ✓ |
| Download project | | ✓ | ✓ | ✓ | ✓ |
......@@ -65,12 +67,17 @@ The following table depicts the various user permission levels in a project.
| Switch visibility level | | | | | ✓ |
| Transfer project to another namespace | | | | | ✓ |
| Remove project | | | | | ✓ |
<<<<<<< HEAD
| Force push to protected branches [^2] | | | | | |
| Remove protected branches [^2] | | | | | |
| Remove pages | | | | | ✓ |
[^1]: If **Public pipelines** is enabled in **Project Settings > CI/CD Pipelines**
[^2]: Not allowed for Guest, Reporter, Developer, Master, or Owner
=======
| Force push to protected branches [^3] | | | | | |
| Remove protected branches [^3] | | | | | |
>>>>>>> ce/master
## Group
......@@ -159,17 +166,20 @@ users:
| Run CI build | | ✓ | ✓ | ✓ |
| Clone source and LFS from current project | | ✓ | ✓ | ✓ |
| Clone source and LFS from public projects | | ✓ | ✓ | ✓ |
| Clone source and LFS from internal projects | | ✓ [^3] | ✓ [^3] | ✓ |
| Clone source and LFS from private projects | | ✓ [^4] | ✓ [^4] | ✓ [^4] |
| Clone source and LFS from internal projects | | ✓ [^4] | ✓ [^4] | ✓ |
| Clone source and LFS from private projects | | ✓ [^5] | ✓ [^5] | ✓ [^5] |
| Push source and LFS | | | | |
| Pull container images from current project | | ✓ | ✓ | ✓ |
| Pull container images from public projects | | ✓ | ✓ | ✓ |
| Pull container images from internal projects| | ✓ [^3] | ✓ [^3] | ✓ |
| Pull container images from private projects | | ✓ [^4] | ✓ [^4] | ✓ [^4] |
| Pull container images from internal projects| | ✓ [^4] | ✓ [^4] | ✓ |
| Pull container images from private projects | | ✓ [^5] | ✓ [^5] | ✓ [^5] |
| Push container images to current project | | ✓ | ✓ | ✓ |
| Push container images to other projects | | | | |
[^3]: Only if user is not external one.
[^4]: Only if user is a member of the project.
[^1]: Guest users can only view the confidential issues they created themselves
[^2]: If **Public pipelines** is enabled in **Project Settings > CI/CD Pipelines**
[^3]: Not allowed for Guest, Reporter, Developer, Master, or Owner
[^4]: Only if user is not external one.
[^5]: Only if user is a member of the project.
[ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994
[new-mod]: project/new_ci_build_permissions_model.md
# Profile settings
## Account
Set up [two-factor authentication](two_factor_authentication.md).
# Two-Factor Authentication
Two-factor Authentication (2FA) provides an additional level of security to your
GitLab account. Once enabled, in addition to supplying your username and
password to login, you'll be prompted for a code generated by an application on
your phone.
By enabling 2FA, the only way someone other than you can log into your account
is to know your username and password *and* have access to your phone.
## Overview
> **Note:**
When you enable 2FA, don't forget to back up your recovery codes.
In addition to a phone application, GitLab supports U2F (universal 2nd factor) devices as
the second factor of authentication. Once enabled, in addition to supplying your username and
password to login, you'll be prompted to activate your U2F device (usually by pressing
a button on it), and it will perform secure authentication on your behalf.
The U2F workflow is only supported by Google Chrome at this point, so we _strongly_ recommend
that you set up both methods of two-factor authentication, so you can still access your account
from other browsers.
## Enabling 2FA
There are two ways to enable two-factor authentication: via a mobile application
or a U2F device.
### Enable 2FA via mobile application
**In GitLab:**
1. Log in to your GitLab account.
1. Go to your **Profile Settings**.
1. Go to **Account**.
1. Click **Enable Two-factor Authentication**.
![Two-factor setup](img/2fa.png)
**On your phone:**
1. Install a compatible application. We recommend [Google Authenticator]
\(proprietary\) or [FreeOTP] \(open source\).
1. In the application, add a new entry in one of two ways:
* Scan the code with your phone's camera to add the entry automatically.
* Enter the details provided to add the entry manually.
**In GitLab:**
1. Enter the six-digit pin number from the entry on your phone into the **Pin
code** field.
1. Click **Submit**.
If the pin you entered was correct, you'll see a message indicating that
Two-Factor Authentication has been enabled, and you'll be presented with a list
of recovery codes.
### Enable 2FA via U2F device
> **Notes:**
- GitLab officially only supports [Yubikey] U2F devices.
- Support for U2F devices was added in GitLab 8.8.
**In GitLab:**
1. Log in to your GitLab account.
1. Go to your **Profile Settings**.
1. Go to **Account**.
1. Click **Enable Two-Factor Authentication**.
1. Plug in your U2F device.
1. Click on **Setup New U2F Device**.
1. A light will start blinking on your device. Activate it by pressing its button.
You will see a message indicating that your device was successfully set up.
Click on **Register U2F Device** to complete the process.
![Two-Factor U2F Setup](img/2fa_u2f_register.png)
## Recovery Codes
> **Note:**
Recovery codes are not generated for U2F devices.
Should you ever lose access to your phone, you can use one of the ten provided
backup codes to login to your account. We suggest copying or printing them for
storage in a safe place. **Each code can be used only once** to log in to your
account.
If you lose the recovery codes or just want to generate new ones, you can do so
from the **Profile settings ➔ Account** page where you first enabled 2FA.
## Logging in with 2FA Enabled
Logging in with 2FA enabled is only slightly different than a normal login.
Enter your username and password credentials as you normally would, and you'll
be presented with a second prompt, depending on which type of 2FA you've enabled.
### Log in via mobile application
Enter the pin from your phone's application or a recovery code to log in.
![Two-Factor Authentication on sign in via OTP](img/2fa_auth.png)
### Log in via U2F device
1. Click **Login via U2F Device**
1. A light will start blinking on your device. Activate it by pressing its button.
You will see a message indicating that your device responded to the authentication request.
Click on **Authenticate via U2F Device** to complete the process.
![Two-Factor Authentication on sign in via U2F device](img/2fa_u2f_authenticate.png)
## Disabling 2FA
1. Log in to your GitLab account.
1. Go to your **Profile Settings**.
1. Go to **Account**.
1. Click **Disable**, under **Two-Factor Authentication**.
This will clear all your two-factor authentication registrations, including mobile
applications and U2F devices.
## Personal access tokens
When 2FA is enabled, you can no longer use your normal account password to
authenticate with Git over HTTPS on the command line, you must use a personal
access token instead.
1. Log in to your GitLab account.
1. Go to your **Profile Settings**.
1. Go to **Access Tokens**.
1. Choose a name and expiry date for the token.
1. Click on **Create Personal Access Token**.
1. Save the personal access token somewhere safe.
When using Git over HTTPS on the command line, enter the personal access token
into the password field.
## Recovery options
If you lose your code generation device (such as your mobile phone) and you need
to disable two-factor authentication on your account, you have several options.
### Use a saved recovery code
When you enabled two-factor authentication for your account, a series of
recovery codes were generated. If you saved those codes somewhere safe, you
may use one to sign in.
First, enter your username/email and password on the GitLab sign in page. When
prompted for a two-factor code, enter one of the recovery codes you saved
previously.
> **Note:** Once a particular recovery code has been used, it cannot be used again.
You may still use the other saved recovery codes at a later time.
### Generate new recovery codes using SSH
It's not uncommon for users to forget to save the recovery codes when enabling
two-factor authentication. If you have an SSH key added to your GitLab account,
you can generate a new set of recovery codes using SSH.
Run `ssh git@gitlab.example.com 2fa_recovery_codes`. You will be prompted to
confirm that you wish to generate new codes. If you choose to continue, any
previously saved codes will be invalidated.
```bash
$ ssh git@gitlab.example.com 2fa_recovery_codes
Are you sure you want to generate new two-factor recovery codes?
Any existing recovery codes you saved will be invalidated. (yes/no)
yes
Your two-factor authentication recovery codes are:
119135e5a3ebce8e
11f6v2a498810dcd
3924c7ab2089c902
e79a3398bfe4f224
34bd7b74adbc8861
f061691d5107df1a
169bf32a18e63e7f
b510e7422e81c947
20dbed24c5e74663
df9d3b9403b9c9f0
During sign in, use one of the codes above when prompted for
your two-factor code. Then, visit your Profile Settings and add
a new device so you do not lose access to your account again.
```
Next, go to the GitLab sign in page and enter your username/email and password.
When prompted for a two-factor code, enter one of the recovery codes obtained
from the command line output.
> **Note:** After signing in, you should immediately visit your **Profile Settings
-> Account** to set up two-factor authentication with a new device.
### Ask a GitLab administrator to disable two-factor on your account
If the above two methods are not possible, you may ask a GitLab global
administrator to disable two-factor authentication for your account. Please
be aware that this will temporarily leave your account in a less secure state.
You should sign in and re-enable two-factor authentication as soon as possible
after the administrator disables it.
## Note to GitLab administrators
You need to take special care to that 2FA keeps working after
[restoring a GitLab backup](../../../raketasks/backup_restore.md).
[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en
[FreeOTP]: https://fedorahosted.org/freeotp/
[YubiKey]: https://www.yubico.com/products/yubikey-hardware/
# Confidential issues
> [Introduced][ce-3282] in GitLab 8.6.
Confidential issues are issues visible only to members of a project with
[sufficient permissions](#permissions-and-access-to-confidential-issues).
Confidential issues can be used by open source projects and companies alike to
keep security vulnerabilities private or prevent surprises from leaking out.
## Making an issue confidential
You can make an issue confidential either by creating a new issue or editing
an existing one.
When you create a new issue, a checkbox right below the text area is available
to mark the issue as confidential. Check that box and hit the **Submit issue**
button to create the issue. For existing issues, edit them, check the
confidential checkbox and hit **Save changes**.
![Creating a new confidential issue](img/confidential_issues_create.png)
## Making an issue non-confidential
To make an issue non-confidential, all you have to do is edit it and unmark
the confidential checkbox. Once you save the issue, it will gain the default
visibility level you have chosen for your project.
Every change from regular to confidential and vice versa, is indicated by a
system note in the issue's comments.
![Confidential issues system notes](img/confidential_issues_system_notes.png)
## Indications of a confidential issue
>**Note:** If you don't have [enough permissions](#permissions-and-access-to-confidential-issues),
you won't be able to see the confidential issues at all.
There are a few things that visually separate a confidential issue from a
regular one. In the issues index page view, you can see the eye-slash icon
next to the issues that are marked as confidential.
![Confidential issues index page](img/confidential_issues_index_page.png)
---
Likewise, while inside the issue, you can see the eye-slash icon right next to
the issue number, but there is also an indicator in the comment area that the
issue you are commenting on is confidential.
![Confidential issue page](img/confidential_issues_issue_page.png)
## Permissions and access to confidential issues
There are two kinds of level access for confidential issues. The general rule
is that confidential issues are visible only to members of a project with at
least [Reporter access][permissions]. However, a guest user can also create
confidential issues, but can only view the ones that they created themselves.
Confidential issues are also hidden in search results for unprivileged users.
For example, here's what a user with Master and Guest access sees in the
project's search results respectively.
| Master access | Guest access |
| :-----------: | :----------: |
| ![Confidential issues search master](img/confidential_issues_search_master.png) | ![Confidential issues search guest](img/confidential_issues_search_guest.png) |
[permissions]: ../../permissions.md#project
[ce-3282]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3282
# Due dates
> [Introduced][ce-3614] in GitLab 8.7.
Due dates can be used in issues to keep track of deadlines and make sure
features are shipped on time. Due dates require at least [Reporter permissions][permissions]
to be able to edit them. On the contrary, they can be seen by everybody.
## Setting a due date
When creating or editing an issue, you can see the due date field from where
a calendar will appear to help you choose the date you want. To remove it,
select the date text and delete it.
![Create a due date](img/due_dates_create.png)
A quicker way to set a due date is via the issue sidebar. Simply expand the
sidebar and select **Edit** to pick a due date or remove the existing one.
Changes are saved immediately.
![Edit a due date via the sidebar](img/due_dates_edit_sidebar.png)
## Making use of due dates
Issues that have a due date can be distinctively seen in the issues index page
with a calendar icon next to them. Issues where the date is past due will have
the icon and the date colored red. You can sort issues by those that are
_Due soon_ or _Due later_ from the dropdown menu in the right.
![Issues with due dates in the issues index page](img/due_dates_issues_index_page.png)
Due dates also appear in your [todos list](../../../workflow/todos.md).
![Issues with due dates in the todos](img/due_dates_todos.png)
[ce-3614]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3614
[permissions]: ../../permissions.md#project
......@@ -21,6 +21,18 @@ request into the source branch, resolving the conflicts using the options
chosen. If the source branch is `feature` and the target branch is `master`,
this is similar to performing `git checkout feature; git merge master` locally.
## Merge conflict editor
> Introduced in GitLab 8.13.
The merge conflict resolution editor allows for more complex merge conflicts,
which require the user to manually modify a file in order to resolve a conflict,
to be solved right form the GitLab interface. Use the **Edit inline** button
to open the editor. Once you're sure about your changes, hit the
**Commit conflict resolution** button.
![Merge conflict editor](img/merge_conflict_editor.png)
## Conflicts available for resolution
GitLab allows resolving conflicts in a file where all of the below are true:
......
......@@ -7,7 +7,14 @@
- [Feature branch workflow](workflow.md)
- [GitLab Flow](gitlab_flow.md)
- [Groups](groups.md)
<<<<<<< HEAD
- [Importing to GitLab](doc/importing/README.md)
=======
- Issues - The GitLab Issue Tracker is an advanced and complete tool for
tracking the evolution of a new idea or the process of solving a problem.
- [Confidential issues](../user/project/issues/confidential_issues.md)
- [Due date for issues](../user/project/issues/due_dates.md)
>>>>>>> ce/master
- [Issue Board](../user/project/issue_board.md)
- [Keyboard shortcuts](shortcuts.md)
- [File finder](file_finder.md)
......
......@@ -129,12 +129,7 @@ module API
end
end
# Delete all merged branches
#
# Parameters:
# id (required) - The ID of a project
# Example Request:
# DELETE /projects/:id/repository/branches/delete_merged
desc 'Delete all merged branches'
delete ":id/repository/merged_branches" do
DeleteMergedBranchesService.new(user_project, current_user).async_execute
......
......@@ -38,26 +38,25 @@ module API
present key, with: Entities::SSHKey
end
# TODO: for 9.0 we should check if params are there with the params block
# grape provides, at this point we'd change behaviour so we can't
# Behaviour now if you don't provide all required params: it renders a
# validation error or two.
desc 'Add new deploy key to currently authenticated user' do
success Entities::SSHKey
end
params do
requires :key, type: String, desc: 'The new deploy key'
requires :title, type: String, desc: 'The name of the deploy key'
end
post ":id/#{path}" do
attrs = attributes_for_keys [:title, :key]
attrs[:key].strip! if attrs[:key]
params[:key].strip!
# Check for an existing key joined to this project
key = user_project.deploy_keys.find_by(key: attrs[:key])
key = user_project.deploy_keys.find_by(key: params[:key])
if key
present key, with: Entities::SSHKey
break
end
# Check for available deploy keys in other projects
key = current_user.accessible_deploy_keys.find_by(key: attrs[:key])
key = current_user.accessible_deploy_keys.find_by(key: params[:key])
if key
user_project.deploy_keys << key
present key, with: Entities::SSHKey
......@@ -65,7 +64,7 @@ module API
end
# Create a new deploy key
key = DeployKey.new attrs
key = DeployKey.new(declared_params(include_missing: false))
if key.valid? && user_project.deploy_keys << key
present key, with: Entities::SSHKey
else
......
......@@ -53,6 +53,10 @@ module Banzai
context[:project]
end
def skip_project_check?
context[:skip_project_check]
end
def reference_class(type)
"gfm gfm-#{type} has-tooltip"
end
......
......@@ -24,7 +24,7 @@ module Banzai
end
def call
return doc if project.nil?
return doc if project.nil? && !skip_project_check?
ref_pattern = User.reference_pattern
ref_pattern_start = /\A#{ref_pattern}\z/
......@@ -58,7 +58,7 @@ module Banzai
# have `gfm` and `gfm-project_member` class names attached for styling.
def user_link_filter(text, link_content: nil)
self.class.references_in(text) do |match, username|
if username == 'all'
if username == 'all' && !skip_project_check?
link_to_all(link_content: link_content)
elsif namespace = namespaces[username]
link_to_namespace(namespace, link_content: link_content) || match
......
......@@ -52,9 +52,9 @@ module Banzai
end
# Same as +render_field+, but without consulting or updating the cache field
def cacheless_render_field(object, field)
def cacheless_render_field(object, field, options = {})
text = object.__send__(field)
context = object.banzai_render_context(field)
context = object.banzai_render_context(field).merge(options)
cacheless_render(text, context)
end
......
......@@ -71,6 +71,14 @@ module Gitlab
)
end
def single_commit_result?
commits_count == 1 && total_result_count == 1
end
def total_result_count
issues_count + merge_requests_count + milestones_count + notes_count + blobs_count + wiki_blobs_count + commits_count
end
private
def blobs
......@@ -98,7 +106,25 @@ module Gitlab
end
def commits
@commits ||= project.repository.find_commits_by_message(query)
@commits ||= find_commits(query)
end
def find_commits(query)
return [] unless Ability.allowed?(@current_user, :download_code, @project)
commits = find_commits_by_message(query)
commit_by_sha = find_commit_by_sha(query)
commits |= [commit_by_sha] if commit_by_sha
commits
end
def find_commits_by_message(query)
project.repository.find_commits_by_message(query)
end
def find_commit_by_sha(query)
key = query.strip
project.repository.commit(key) if Commit.valid_hash?(key)
end
def project_ids_relation
......
......@@ -43,6 +43,10 @@ module Gitlab
@milestones_count ||= milestones.count
end
def single_commit_result?
false
end
private
def projects
......
......@@ -8,21 +8,31 @@ module Mattermost
@user = user
end
private
def with_session(&blk)
Mattermost::Session.new(user).with_session(&blk)
end
def json_get(path, options = {})
private
# Should be used in a session manually
def get(session, path, options = {})
json_response session.get(path, options)
end
# Should be used in a session manually
def post(session, path, options = {})
json_response session.post(path, options)
end
def session_get(path, options = {})
with_session do |session|
json_response session.get(path, options)
get(session, path, options)
end
end
def json_post(path, options = {})
def session_post(path, options = {})
with_session do |session|
json_response session.post(path, options)
post(session, path, options)
end
end
......
module Mattermost
class Command < Client
def create(params)
response = json_post("/api/v3/teams/#{params[:team_id]}/commands/create",
response = session_post("/api/v3/teams/#{params[:team_id]}/commands/create",
body: params.to_json)
response['token']
......
module Mattermost
class Team < Client
def all
json_get('/api/v3/teams/all')
session_get('/api/v3/teams/all')
end
end
end
......@@ -11,8 +11,10 @@ namespace :gitlab do
gem_version = run_command(%W(gem --version))
# check Bundler version
bunder_version = run_and_match(%W(bundle --version), /[\d\.]+/).try(:to_s)
# check Bundler version
# check Rake version
rake_version = run_and_match(%W(rake --version), /[\d\.]+/).try(:to_s)
# check redis version
redis_version = run_and_match(%W(redis-cli --version), /redis-cli (\d+\.\d+\.\d+)/).to_a
puts ""
puts "System information".color(:yellow)
......@@ -24,6 +26,7 @@ namespace :gitlab do
puts "Gem Version:\t#{gem_version || "unknown".color(:red)}"
puts "Bundler Version:#{bunder_version || "unknown".color(:red)}"
puts "Rake Version:\t#{rake_version || "unknown".color(:red)}"
puts "Redis Version:\t#{redis_version[1] || "unknown".color(:red)}"
puts "Sidekiq Version:#{Sidekiq::VERSION}"
......
......@@ -13,6 +13,7 @@ FactoryGirl.define do
factory :note_on_issue, traits: [:on_issue], aliases: [:votable_note]
factory :note_on_merge_request, traits: [:on_merge_request]
factory :note_on_project_snippet, traits: [:on_project_snippet]
factory :note_on_personal_snippet, traits: [:on_personal_snippet]
factory :system_note, traits: [:system]
factory :legacy_diff_note_on_commit, traits: [:on_commit, :legacy_diff_note], class: LegacyDiffNote
......@@ -70,6 +71,11 @@ FactoryGirl.define do
noteable { create(:project_snippet, project: project) }
end
trait :on_personal_snippet do
noteable { create(:personal_snippet) }
project nil
end
trait :system do
system true
end
......
......@@ -40,6 +40,16 @@ describe 'Dropdown label', js: true, feature: true do
visit namespace_project_issues_path(project.namespace, project)
end
describe 'keyboard navigation' do
it 'selects label' do
send_keys_to_filtered_search('label:')
filtered_search.native.send_keys(:down, :down, :enter)
expect(filtered_search.value).to eq("label:~#{special_label.name} ")
end
end
describe 'behavior' do
it 'opens when the search bar has label:' do
filtered_search.set('label:')
......
......@@ -20,6 +20,22 @@ describe 'Search bar', js: true, feature: true do
left_style.to_s.gsub('left: ', '').to_f
end
describe 'keyboard navigation' do
it 'makes item active' do
filtered_search.native.send_keys(:down)
page.within '#js-dropdown-hint' do
expect(page).to have_selector('.dropdown-active')
end
end
it 'selects item' do
filtered_search.native.send_keys(:down, :down, :enter)
expect(filtered_search.value).to eq('author:')
end
end
describe 'clear search button' do
it 'clears text' do
search_text = 'search_text'
......
require 'spec_helper'
feature 'Merge Request button', feature: true do
shared_examples 'Merge Request button only shown when allowed' do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:forked_project) { create(:project, :public, forked_from_project: project) }
context 'not logged in' do
it 'does not show Create Merge Request button' do
visit url
within("#content-body") do
expect(page).not_to have_link(label)
end
end
end
context 'logged in as developer' do
before do
login_as(user)
project.team << [user, :developer]
end
it 'shows Create Merge Request button' do
href = new_namespace_project_merge_request_path(project.namespace,
project,
merge_request: { source_branch: 'feature',
target_branch: 'master' })
visit url
within("#content-body") do
expect(page).to have_link(label, href: href)
end
end
context 'merge requests are disabled' do
before do
project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED)
end
it 'does not show Create Merge Request button' do
visit url
within("#content-body") do
expect(page).not_to have_link(label)
end
end
end
end
context 'logged in as non-member' do
before do
login_as(user)
end
it 'does not show Create Merge Request button' do
visit url
within("#content-body") do
expect(page).not_to have_link(label)
end
end
context 'on own fork of project' do
let(:user) { forked_project.owner }
it 'shows Create Merge Request button' do
href = new_namespace_project_merge_request_path(forked_project.namespace,
forked_project,
merge_request: { source_branch: 'feature',
target_branch: 'master' })
visit fork_url
within("#content-body") do
expect(page).to have_link(label, href: href)
end
end
end
end
end
context 'on branches page' do
it_behaves_like 'Merge Request button only shown when allowed' do
let(:label) { 'Merge Request' }
let(:url) { namespace_project_branches_path(project.namespace, project) }
let(:fork_url) { namespace_project_branches_path(forked_project.namespace, forked_project) }
end
end
context 'on compare page' do
it_behaves_like 'Merge Request button only shown when allowed' do
let(:label) { 'Create Merge Request' }
let(:url) { namespace_project_compare_path(project.namespace, project, from: 'master', to: 'feature') }
let(:fork_url) { namespace_project_compare_path(forked_project.namespace, forked_project, from: 'master', to: 'feature') }
end
end
context 'on commits page' do
it_behaves_like 'Merge Request button only shown when allowed' do
let(:label) { 'Create Merge Request' }
let(:url) { namespace_project_commits_path(project.namespace, project, 'feature') }
let(:fork_url) { namespace_project_commits_path(forked_project.namespace, forked_project, 'feature') }
end
end
end
......@@ -211,4 +211,44 @@ describe "Search", feature: true do
end
end
end
describe 'search for commits' do
before do
visit search_path(project_id: project.id)
end
it 'redirects to commit page when search by sha and only commit found' do
fill_in 'search', with: '6d394385cf567f80a8fd85055db1ab4c5295806f'
click_button 'Search'
expect(page).to have_current_path(namespace_project_commit_path(project.namespace, project, '6d394385cf567f80a8fd85055db1ab4c5295806f'))
end
it 'redirects to single commit regardless of query case' do
fill_in 'search', with: '6D394385cf'
click_button 'Search'
expect(page).to have_current_path(namespace_project_commit_path(project.namespace, project, '6d394385cf567f80a8fd85055db1ab4c5295806f'))
end
it 'holds on /search page when the only commit is found by message' do
create_commit('Message referencing another sha: "deadbeef" ', project, user, 'master')
fill_in 'search', with: 'deadbeef'
click_button 'Search'
expect(page).to have_current_path('/search', only_path: true)
end
it 'shows multiple matching commits' do
fill_in 'search', with: 'See merge request'
click_button 'Search'
click_link 'Commits'
expect(page).to have_selector('.commit-row-description', count: 9)
end
end
end
......@@ -36,6 +36,19 @@ feature 'Task Lists', feature: true do
MARKDOWN
end
let(:nested_tasks_markdown) do
<<-EOT.strip_heredoc
- [ ] Task a
- [x] Task a.1
- [ ] Task a.2
- [ ] Task b
1. [ ] Task 1
1. [ ] Task 1.1
1. [x] Task 1.2
EOT
end
before do
Warden.test_mode!
......@@ -123,6 +136,35 @@ feature 'Task Lists', feature: true do
expect(page).to have_content("1 of 1 task completed")
end
end
describe 'nested tasks', js: true do
let(:issue) { create(:issue, description: nested_tasks_markdown, author: user, project: project) }
before { visit_issue(project, issue) }
it 'renders' do
expect(page).to have_selector('ul.task-list', count: 2)
expect(page).to have_selector('li.task-list-item', count: 7)
expect(page).to have_selector('ul input[checked]', count: 1)
expect(page).to have_selector('ol input[checked]', count: 1)
end
it 'solves tasks' do
expect(page).to have_content("2 of 7 tasks completed")
page.find('li.task-list-item', text: 'Task b').find('input').click
page.find('li.task-list-item ul li.task-list-item', text: 'Task a.2').find('input').click
page.find('li.task-list-item ol li.task-list-item', text: 'Task 1.1').find('input').click
expect(page).to have_content("5 of 7 tasks completed")
visit_issue(project, issue) # reload to see new system notes
expect(page).to have_content('marked the task Task b as complete')
expect(page).to have_content('marked the task Task a.2 as complete')
expect(page).to have_content('marked the task Task 1.1 as complete')
end
end
end
describe 'for Notes' do
......@@ -236,7 +278,7 @@ feature 'Task Lists', feature: true do
expect(page).to have_content("2 of 6 tasks completed")
end
end
describe 'single incomplete task' do
let!(:merge) { create(:merge_request, :simple, description: singleIncompleteMarkdown, author: user, source_project: project) }
......
......@@ -21,7 +21,6 @@
messages = $('.abuse-reports .message');
});
it('should truncate long messages', () => {
const $longMessage = findMessage('LONG MESSAGE');
expect($longMessage.data('original-message')).toEqual(jasmine.anything());
......
......@@ -33,7 +33,6 @@ describe('Rollback Component', () => {
expect(component.$el.querySelector('span').textContent).toContain('Re-deploy');
});
it('Should render Rollback label when isLastDeployment is false', () => {
const component = new window.gl.environmentsList.RollbackComponent({
el: document.querySelector('.test-dom-element'),
......
......@@ -52,5 +52,22 @@
expect(value).toBe(null);
});
});
describe('gl.utils.normalizedHeaders', () => {
it('should upperCase all the header keys to keep them consistent', () => {
const apiHeaders = {
'X-Something-Workhorse': { workhorse: 'ok' },
'x-something-nginx': { nginx: 'ok' },
};
const normalized = gl.utils.normalizeHeaders(apiHeaders);
const WORKHORSE = 'X-SOMETHING-WORKHORSE';
const NGINX = 'X-SOMETHING-NGINX';
expect(normalized[WORKHORSE].workhorse).toBe('ok');
expect(normalized[NGINX].nginx).toBe('ok');
});
});
});
})();
......@@ -152,6 +152,30 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
end
end
context 'when a project is not specified' do
let(:project) { nil }
it 'does not link a User' do
doc = reference_filter("Hey #{reference}")
expect(doc).not_to include('a')
end
context 'when skip_project_check set to true' do
it 'links to a User' do
doc = reference_filter("Hey #{reference}", skip_project_check: true)
expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
end
it 'does not link users using @all reference' do
doc = reference_filter("Hey #{User.reference_prefix}all", skip_project_check: true)
expect(doc).not_to include('a')
end
end
end
describe '#namespaces' do
it 'returns a Hash containing all Namespaces' do
document = Nokogiri::HTML.fragment("<p>#{user.to_reference}</p>")
......
......@@ -179,4 +179,119 @@ describe Gitlab::ProjectSearchResults, lib: true do
expect(results.objects('notes')).not_to include note
end
end
# Examples for commit access level test
#
# params:
# * search_phrase
# * commit
#
shared_examples 'access restricted commits' do
context 'when project is internal' do
let(:project) { create(:project, :internal) }
it 'does not search if user is not authenticated' do
commits = described_class.new(nil, project, search_phrase).objects('commits')
expect(commits).to be_empty
end
it 'searches if user is authenticated' do
commits = described_class.new(user, project, search_phrase).objects('commits')
expect(commits).to contain_exactly commit
end
end
context 'when project is private' do
let!(:creator) { create(:user, username: 'private-project-author') }
let!(:private_project) { create(:project, :private, creator: creator, namespace: creator.namespace) }
let(:team_master) do
user = create(:user, username: 'private-project-master')
private_project.team << [user, :master]
user
end
let(:team_reporter) do
user = create(:user, username: 'private-project-reporter')
private_project.team << [user, :reporter]
user
end
it 'does not show commit to stranger' do
commits = described_class.new(nil, private_project, search_phrase).objects('commits')
expect(commits).to be_empty
end
context 'team access' do
it 'shows commit to creator' do
commits = described_class.new(creator, private_project, search_phrase).objects('commits')
expect(commits).to contain_exactly commit
end
it 'shows commit to master' do
commits = described_class.new(team_master, private_project, search_phrase).objects('commits')
expect(commits).to contain_exactly commit
end
it 'shows commit to reporter' do
commits = described_class.new(team_reporter, private_project, search_phrase).objects('commits')
expect(commits).to contain_exactly commit
end
end
end
end
describe 'commit search' do
context 'by commit message' do
let(:project) { create(:project, :public) }
let(:commit) { project.repository.commit('59e29889be61e6e0e5e223bfa9ac2721d31605b8') }
let(:message) { 'Sorry, I did a mistake' }
it 'finds commit by message' do
commits = described_class.new(user, project, message).objects('commits')
expect(commits).to contain_exactly commit
end
it 'handles when no commit match' do
commits = described_class.new(user, project, 'not really an existing description').objects('commits')
expect(commits).to be_empty
end
it_behaves_like 'access restricted commits' do
let(:search_phrase) { message }
let(:commit) { project.repository.commit('59e29889be61e6e0e5e223bfa9ac2721d31605b8') }
end
end
context 'by commit hash' do
let(:project) { create(:project, :public) }
let(:commit) { project.repository.commit('0b4bc9a') }
commit_hashes = { short: '0b4bc9a', full: '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
commit_hashes.each do |type, commit_hash|
it "shows commit by #{type} hash id" do
commits = described_class.new(user, project, commit_hash).objects('commits')
expect(commits).to contain_exactly commit
end
end
it 'handles not existing commit hash correctly' do
commits = described_class.new(user, project, 'deadbeef').objects('commits')
expect(commits).to be_empty
end
it_behaves_like 'access restricted commits' do
let(:search_phrase) { '0b4bc9a49' }
let(:commit) { project.repository.commit('0b4bc9a') }
end
end
end
end
......@@ -171,6 +171,33 @@ describe Ability, lib: true do
end
end
describe '.users_that_can_read_personal_snippet' do
def users_for_snippet(snippet)
described_class.users_that_can_read_personal_snippet(users, snippet)
end
let(:users) { create_list(:user, 3) }
let(:author) { users[0] }
it 'private snippet is readable only by its author' do
snippet = create(:personal_snippet, :private, author: author)
expect(users_for_snippet(snippet)).to match_array([author])
end
it 'internal snippet is readable by all registered users' do
snippet = create(:personal_snippet, :public, author: author)
expect(users_for_snippet(snippet)).to match_array(users)
end
it 'public snippet is readable by all users' do
snippet = create(:personal_snippet, :public, author: author)
expect(users_for_snippet(snippet)).to match_array(users)
end
end
describe '.issues_readable_by_user' do
context 'with an admin user' do
it 'returns all given issues' do
......
......@@ -351,4 +351,22 @@ eos
expect(commit).not_to be_work_in_progress
end
end
describe '.valid_hash?' do
it 'checks hash contents' do
expect(described_class.valid_hash?('abcdef01239ABCDEF')).to be true
expect(described_class.valid_hash?("abcdef01239ABCD\nEF")).to be false
expect(described_class.valid_hash?(' abcdef01239ABCDEF ')).to be false
expect(described_class.valid_hash?('Gabcdef01239ABCDEF')).to be false
expect(described_class.valid_hash?('gabcdef01239ABCDEF')).to be false
expect(described_class.valid_hash?('-abcdef01239ABCDEF')).to be false
end
it 'checks hash length' do
expect(described_class.valid_hash?('a' * 6)).to be false
expect(described_class.valid_hash?('a' * 7)).to be true
expect(described_class.valid_hash?('a' * 40)).to be true
expect(described_class.valid_hash?('a' * 41)).to be false
end
end
end
......@@ -30,12 +30,20 @@ describe Issue, "Mentionable" do
describe '#mentioned_users' do
let!(:user) { create(:user, username: 'stranger') }
let!(:user2) { create(:user, username: 'john') }
let!(:issue) { create(:issue, description: "#{user.to_reference} mentioned") }
let!(:user3) { create(:user, username: 'jim') }
let(:issue) { create(:issue, description: "#{user.to_reference} mentioned") }
subject { issue.mentioned_users }
it { is_expected.to include(user) }
it { is_expected.not_to include(user2) }
it { expect(subject).to contain_exactly(user) }
context 'when a note on personal snippet' do
let!(:note) { create(:note_on_personal_snippet, note: "#{user.to_reference} mentioned #{user3.to_reference}") }
subject { note.mentioned_users }
it { expect(subject).to contain_exactly(user, user3) }
end
end
describe '#referenced_mentionables' do
......@@ -138,6 +146,16 @@ describe Issue, "Mentionable" do
issue.update_attributes(description: issues[1].to_reference)
issue.create_new_cross_references!
end
it 'notifies new references from project snippet note' do
snippet = create(:snippet, project: project)
note = create(:note, note: issues[0].to_reference, noteable: snippet, project: project, author: author)
expect(SystemNoteService).to receive(:cross_reference).with(issues[1], any_args)
note.update_attributes(note: issues[1].to_reference)
note.create_new_cross_references!
end
end
def create_issue(description:)
......
......@@ -68,4 +68,14 @@ describe Group, 'Routable' do
end
end
end
describe '.member_descendants' do
let!(:user) { create(:user) }
let!(:nested_group) { create(:group, parent: group) }
before { group.add_owner(user) }
subject { described_class.member_descendants(user.id) }
it { is_expected.to eq([nested_group]) }
end
end
......@@ -305,6 +305,12 @@ describe Group, models: true do
it 'returns the canonical URL' do
expect(group.web_url).to include("groups/#{group.name}")
end
context 'nested group' do
let(:nested_group) { create(:group, :nested) }
it { expect(nested_group.web_url).to include("groups/#{nested_group.full_path}") }
end
end
describe 'nested group' do
......
......@@ -5,6 +5,8 @@ describe Namespace, models: true do
it { is_expected.to have_many :projects }
it { is_expected.to have_many :project_statistics }
it { is_expected.to belong_to :parent }
it { is_expected.to have_many :children }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) }
......@@ -201,17 +203,31 @@ describe Namespace, models: true do
it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") }
end
describe '#parents' do
describe '#ancestors' do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
let(:deep_nested_group) { create(:group, parent: nested_group) }
let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
it 'returns the correct parents' do
expect(very_deep_nested_group.parents).to eq([group, nested_group, deep_nested_group])
expect(deep_nested_group.parents).to eq([group, nested_group])
expect(nested_group.parents).to eq([group])
expect(group.parents).to eq([])
it 'returns the correct ancestors' do
expect(very_deep_nested_group.ancestors).to eq([group, nested_group, deep_nested_group])
expect(deep_nested_group.ancestors).to eq([group, nested_group])
expect(nested_group.ancestors).to eq([group])
expect(group.ancestors).to eq([])
end
end
describe '#descendants' do
let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
let!(:deep_nested_group) { create(:group, parent: nested_group) }
let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
it 'returns the correct descendants' do
expect(very_deep_nested_group.descendants.to_a).to eq([])
expect(deep_nested_group.descendants.to_a).to eq([very_deep_nested_group])
expect(nested_group.descendants.to_a).to eq([deep_nested_group, very_deep_nested_group])
expect(group.descendants.to_a).to eq([nested_group, deep_nested_group, very_deep_nested_group])
end
end
end
......@@ -52,6 +52,19 @@ describe Note, models: true do
subject { create(:note) }
it { is_expected.to be_valid }
end
context 'when project is missing for a project related note' do
subject { build(:note, project: nil, noteable: build_stubbed(:issue)) }
it { is_expected.to be_invalid }
end
context 'when noteable is a personal snippet' do
subject { build(:note_on_personal_snippet) }
it 'is valid without project' do
is_expected.to be_valid
end
end
end
describe "Commit notes" do
......@@ -139,6 +152,7 @@ describe Note, models: true do
with([{
text: note1.note,
context: {
skip_project_check: false,
pipeline: :note,
cache_key: [note1, "note"],
project: note1.project,
......@@ -150,6 +164,7 @@ describe Note, models: true do
with([{
text: note2.note,
context: {
skip_project_check: false,
pipeline: :note,
cache_key: [note2, "note"],
project: note2.project,
......@@ -306,4 +321,70 @@ describe Note, models: true do
end
end
end
describe '#for_personal_snippet?' do
it 'returns false for a project snippet note' do
expect(build(:note_on_project_snippet).for_personal_snippet?).to be_falsy
end
it 'returns true for a personal snippet note' do
expect(build(:note_on_personal_snippet).for_personal_snippet?).to be_truthy
end
end
describe '#to_ability_name' do
it 'returns snippet for a project snippet note' do
expect(build(:note_on_project_snippet).to_ability_name).to eq('snippet')
end
it 'returns personal_snippet for a personal snippet note' do
expect(build(:note_on_personal_snippet).to_ability_name).to eq('personal_snippet')
end
it 'returns merge_request for an MR note' do
expect(build(:note_on_merge_request).to_ability_name).to eq('merge_request')
end
it 'returns issue for an issue note' do
expect(build(:note_on_issue).to_ability_name).to eq('issue')
end
it 'returns issue for a commit note' do
expect(build(:note_on_commit).to_ability_name).to eq('commit')
end
end
describe '#cache_markdown_field' do
let(:html) { '<p>some html</p>'}
context 'note for a project snippet' do
let(:note) { build(:note_on_project_snippet) }
before do
expect(Banzai::Renderer).to receive(:cacheless_render_field).
with(note, :note, { skip_project_check: false }).and_return(html)
note.save
end
it 'creates a note' do
expect(note.note_html).to eq(html)
end
end
context 'note for a personal snippet' do
let(:note) { build(:note_on_personal_snippet) }
before do
expect(Banzai::Renderer).to receive(:cacheless_render_field).
with(note, :note, { skip_project_check: true }).and_return(html)
note.save
end
it 'creates a note' do
expect(note.note_html).to eq(html)
end
end
end
end
......@@ -14,7 +14,7 @@ describe Route, models: true do
it { is_expected.to validate_uniqueness_of(:path) }
end
describe '#rename_children' do
describe '#rename_descendants' do
let!(:nested_group) { create(:group, path: "test", parent: group) }
let!(:deep_nested_group) { create(:group, path: "foo", parent: nested_group) }
let!(:similar_group) { create(:group, path: 'gitlab-org') }
......
......@@ -811,14 +811,14 @@ describe User, models: true do
describe '#avatar_type' do
let(:user) { create(:user) }
it "is true if avatar is image" do
it 'is true if avatar is image' do
user.update_attribute(:avatar, 'uploads/avatar.png')
expect(user.avatar_type).to be_truthy
end
it "is false if avatar is html page" do
it 'is false if avatar is html page' do
user.update_attribute(:avatar, 'uploads/avatar.html')
expect(user.avatar_type).to eq(["only images allowed"])
expect(user.avatar_type).to eq(['only images allowed'])
end
end
......@@ -969,8 +969,8 @@ describe User, models: true do
end
end
describe "#starred?" do
it "determines if user starred a project" do
describe '#starred?' do
it 'determines if user starred a project' do
user = create :user
project1 = create(:empty_project, :public)
project2 = create(:empty_project, :public)
......@@ -996,8 +996,8 @@ describe User, models: true do
end
end
describe "#toggle_star" do
it "toggles stars" do
describe '#toggle_star' do
it 'toggles stars' do
user = create :user
project = create(:empty_project, :public)
......@@ -1009,6 +1009,7 @@ describe User, models: true do
end
end
<<<<<<< HEAD
describe "#existing_member?" do
it "returns true for exisitng user" do
create :user, email: "bruno@example.com"
......@@ -1031,30 +1032,46 @@ describe User, models: true do
end
describe "#sort" do
=======
describe '#sort' do
>>>>>>> ce/master
before do
User.delete_all
@user = create :user, created_at: Date.today, last_sign_in_at: Date.today, name: 'Alpha'
@user1 = create :user, created_at: Date.today - 1, last_sign_in_at: Date.today - 1, name: 'Omega'
@user2 = create :user, created_at: Date.today - 2, last_sign_in_at: nil, name: 'Beta'
end
it "sorts users by the recent sign-in time" do
expect(User.sort('recent_sign_in').first).to eq(@user)
context 'when sort by recent_sign_in' do
it 'sorts users by the recent sign-in time' do
expect(User.sort('recent_sign_in').first).to eq(@user)
end
it 'pushes users who never signed in to the end' do
expect(User.sort('recent_sign_in').third).to eq(@user2)
end
end
it "sorts users by the oldest sign-in time" do
expect(User.sort('oldest_sign_in').first).to eq(@user1)
context 'when sort by oldest_sign_in' do
it 'sorts users by the oldest sign-in time' do
expect(User.sort('oldest_sign_in').first).to eq(@user1)
end
it 'pushes users who never signed in to the end' do
expect(User.sort('oldest_sign_in').third).to eq(@user2)
end
end
it "sorts users in descending order by their creation time" do
it 'sorts users in descending order by their creation time' do
expect(User.sort('created_desc').first).to eq(@user)
end
it "sorts users in ascending order by their creation time" do
expect(User.sort('created_asc').first).to eq(@user1)
it 'sorts users in ascending order by their creation time' do
expect(User.sort('created_asc').first).to eq(@user2)
end
it "sorts users by id in descending order when nil is passed" do
expect(User.sort(nil).first).to eq(@user1)
it 'sorts users by id in descending order when nil is passed' do
expect(User.sort(nil).first).to eq(@user2)
end
end
......@@ -1414,6 +1431,39 @@ describe User, models: true do
end
end
describe '#nested_groups' do
let!(:user) { create(:user) }
let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
before do
group.add_owner(user)
# Add more data to ensure method does not include wrong groups
create(:group).add_owner(create(:user))
end
it { expect(user.nested_groups).to eq([nested_group]) }
end
describe '#nested_projects' do
let!(:user) { create(:user) }
let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
let!(:project) { create(:project, namespace: group) }
let!(:nested_project) { create(:project, namespace: nested_group) }
before do
group.add_owner(user)
# Add more data to ensure method does not include wrong projects
other_project = create(:project, namespace: create(:group, :nested))
other_project.add_developer(create(:user))
end
it { expect(user.nested_projects).to eq([nested_project]) }
end
describe '#refresh_authorized_projects', redis: true do
let(:project1) { create(:empty_project) }
let(:project2) { create(:empty_project) }
......
......@@ -73,19 +73,14 @@ describe API::DeployKeys, api: true do
post api("/projects/#{project.id}/deploy_keys", admin), { title: 'invalid key' }
expect(response).to have_http_status(400)
expect(json_response['message']['key']).to eq([
'can\'t be blank',
'is invalid'
])
expect(json_response['error']).to eq('key is missing')
end
it 'should not create a key without title' do
post api("/projects/#{project.id}/deploy_keys", admin), key: 'some key'
expect(response).to have_http_status(400)
expect(json_response['message']['title']).to eq([
'can\'t be blank'
])
expect(json_response['error']).to eq('title is missing')
end
it 'should create new ssh key' do
......
......@@ -13,7 +13,12 @@ describe 'cycle analytics events' do
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
3.times { create_cycle }
3.times do |count|
Timecop.freeze(Time.now + count.days) do
create_cycle
end
end
deploy_master
login_as(user)
......
......@@ -133,12 +133,18 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request.notes).to be_empty }
it { expect(@merge_request).to be_open }
<<<<<<< HEAD
it { expect(@merge_request.approvals).not_to be_empty }
=======
>>>>>>> ce/master
it { expect(@fork_merge_request.notes.last.note).to include('added 28 commits') }
it { expect(@fork_merge_request).to be_open }
it { expect(@build_failed_todo).to be_pending }
it { expect(@fork_build_failed_todo).to be_pending }
<<<<<<< HEAD
it { expect(@fork_merge_request.approvals).to be_empty }
=======
>>>>>>> ce/master
end
context 'closed fork merge request' do
......@@ -155,12 +161,18 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request.notes).to be_empty }
it { expect(@merge_request).to be_open }
<<<<<<< HEAD
it { expect(@merge_request.approvals).not_to be_empty }
=======
>>>>>>> ce/master
it { expect(@fork_merge_request.notes).to be_empty }
it { expect(@fork_merge_request).to be_closed }
it { expect(@build_failed_todo).to be_pending }
it { expect(@fork_build_failed_todo).to be_pending }
<<<<<<< HEAD
it { expect(@fork_merge_request.approvals).to be_empty }
=======
>>>>>>> ce/master
end
end
......
......@@ -15,39 +15,45 @@ describe Notes::CreateService, services: true do
context "valid params" do
it 'returns a valid note' do
note = Notes::CreateService.new(project, user, opts).execute
note = described_class.new(project, user, opts).execute
expect(note).to be_valid
end
it 'returns a persisted note' do
note = Notes::CreateService.new(project, user, opts).execute
note = described_class.new(project, user, opts).execute
expect(note).to be_persisted
end
it 'note has valid content' do
note = Notes::CreateService.new(project, user, opts).execute
note = described_class.new(project, user, opts).execute
expect(note.note).to eq(opts[:note])
end
it 'note belongs to the correct project' do
note = described_class.new(project, user, opts).execute
expect(note.project).to eq(project)
end
it 'TodoService#new_note is called' do
note = build(:note)
allow(project).to receive_message_chain(:notes, :new).with(opts) { note }
note = build(:note, project: project)
allow(Note).to receive(:new).with(opts) { note }
expect_any_instance_of(TodoService).to receive(:new_note).with(note, user)
Notes::CreateService.new(project, user, opts).execute
described_class.new(project, user, opts).execute
end
it 'enqueues NewNoteWorker' do
note = build(:note, id: 999)
allow(project).to receive_message_chain(:notes, :new).with(opts) { note }
note = build(:note, id: 999, project: project)
allow(Note).to receive(:new).with(opts) { note }
expect(NewNoteWorker).to receive(:perform_async).with(note.id)
Notes::CreateService.new(project, user, opts).execute
described_class.new(project, user, opts).execute
end
end
......@@ -75,6 +81,27 @@ describe Notes::CreateService, services: true do
end
end
end
describe 'personal snippet note' do
subject { described_class.new(nil, user, params).execute }
let(:snippet) { create(:personal_snippet) }
let(:params) do
{ note: 'comment', noteable_type: 'Snippet', noteable_id: snippet.id }
end
it 'returns a valid note' do
expect(subject).to be_valid
end
it 'returns a persisted note' do
expect(subject).to be_persisted
end
it 'note has valid content' do
expect(subject.note).to eq(params[:note])
end
end
end
describe "award emoji" do
......@@ -88,7 +115,7 @@ describe Notes::CreateService, services: true do
noteable_type: 'Issue',
noteable_id: issue.id
}
note = Notes::CreateService.new(project, user, opts).execute
note = described_class.new(project, user, opts).execute
expect(note).to be_valid
expect(note.name).to eq('smile')
......@@ -100,7 +127,7 @@ describe Notes::CreateService, services: true do
noteable_type: 'Issue',
noteable_id: issue.id
}
note = Notes::CreateService.new(project, user, opts).execute
note = described_class.new(project, user, opts).execute
expect(note).to be_valid
expect(note.note).to eq(opts[:note])
......@@ -115,7 +142,7 @@ describe Notes::CreateService, services: true do
expect_any_instance_of(TodoService).to receive(:new_award_emoji).with(issue, user)
Notes::CreateService.new(project, user, opts).execute
described_class.new(project, user, opts).execute
end
end
end
......@@ -269,6 +269,55 @@ describe NotificationService, services: true do
end
end
context 'personal snippet note' do
let(:snippet) { create(:personal_snippet, :public, author: @u_snippet_author) }
let(:note) { create(:note_on_personal_snippet, noteable: snippet, note: '@mentioned note', author: @u_note_author) }
before do
@u_watcher = create_global_setting_for(create(:user), :watch)
@u_participant = create_global_setting_for(create(:user), :participating)
@u_disabled = create_global_setting_for(create(:user), :disabled)
@u_mentioned = create_global_setting_for(create(:user, username: 'mentioned'), :mention)
@u_mentioned_level = create_global_setting_for(create(:user, username: 'participator'), :mention)
@u_note_author = create(:user, username: 'note_author')
@u_snippet_author = create(:user, username: 'snippet_author')
@u_not_mentioned = create_global_setting_for(create(:user, username: 'regular'), :participating)
reset_delivered_emails!
end
let!(:notes) do
[
create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_watcher),
create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_participant),
create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_mentioned),
create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_disabled),
create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_note_author),
]
end
describe '#new_note' do
it 'notifies the participants' do
notification.new_note(note)
# it emails participants
should_email(@u_watcher)
should_email(@u_participant)
should_email(@u_watcher)
should_email(@u_snippet_author)
# it emails mentioned users
should_email(@u_mentioned)
# it does not email participants with mention notification level
should_not_email(@u_mentioned_level)
# it does not email note author
should_not_email(@u_note_author)
end
end
end
context 'commit note' do
let(:project) { create(:project, :public) }
let(:note) { create(:note_on_commit, project: project) }
......
......@@ -33,6 +33,30 @@ shared_examples 'a Taskable' do
end
end
describe 'with nested tasks' do
before do
subject.description = <<-EOT.strip_heredoc
- [ ] Task a
- [x] Task a.1
- [ ] Task a.2
- [ ] Task b
1. [ ] Task 1
1. [ ] Task 1.1
1. [ ] Task 1.2
1. [x] Task 2
1. [x] Task 2.1
EOT
end
it 'returns the correct task status' do
expect(subject.task_status).to match('3 of')
expect(subject.task_status).to match('9 tasks completed')
expect(subject.task_status_short).to match('3/')
expect(subject.task_status_short).to match('9 tasks')
end
end
describe 'with an incomplete task' do
before do
subject.description = <<-EOT.strip_heredoc
......
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