Commit 5348daad authored by Lin Jen-Shin (godfat)'s avatar Lin Jen-Shin (godfat)

Merge branch 'ce-to-ee-2017-09-29' into 'master'

CE upstream: Friday

Closes gitaly#601, gitaly#562, #3573, #3118, and gitlab-ce#38507

See merge request gitlab-org/gitlab-ee!3036
parents 3f8c0079 f78b006f
...@@ -8,4 +8,4 @@ ...@@ -8,4 +8,4 @@
karma.config.js karma.config.js
webpack.config.js webpack.config.js
svg.config.js svg.config.js
/app/assets/javascripts/locale/**/*.js /app/assets/javascripts/locale/**/app.js
...@@ -40,10 +40,10 @@ export default () => { ...@@ -40,10 +40,10 @@ export default () => {
class="text-center" class="text-center"
v-if="error"> v-if="error">
<span v-if="loadError"> <span v-if="loadError">
An error occured whilst loading the file. Please try again later. An error occurred whilst loading the file. Please try again later.
</span> </span>
<span v-else> <span v-else>
An error occured whilst parsing the file. An error occurred whilst parsing the file.
</span> </span>
</p> </p>
</div> </div>
......
...@@ -48,10 +48,10 @@ export default () => { ...@@ -48,10 +48,10 @@ export default () => {
class="text-center" class="text-center"
v-if="error"> v-if="error">
<span v-if="loadError"> <span v-if="loadError">
An error occured whilst loading the file. Please try again later. An error occurred whilst loading the file. Please try again later.
</span> </span>
<span v-else> <span v-else>
An error occured whilst decoding the file. An error occurred whilst decoding the file.
</span> </span>
</p> </p>
</div> </div>
......
...@@ -96,7 +96,7 @@ export default { ...@@ -96,7 +96,7 @@ export default {
<div class="flash-container" <div class="flash-container"
v-if="error"> v-if="error">
<div class="flash-alert"> <div class="flash-alert">
An error occured. Please try again. An error occurred. Please try again.
</div> </div>
</div> </div>
<label class="label-light" <label class="label-light"
......
...@@ -167,7 +167,7 @@ window.Build = (function () { ...@@ -167,7 +167,7 @@ window.Build = (function () {
Build.prototype.getBuildTrace = function () { Build.prototype.getBuildTrace = function () {
return $.ajax({ return $.ajax({
url: `${this.pageUrl}/trace.json`, url: `${this.pageUrl}/trace.json`,
data: this.state, data: { state: this.state },
}) })
.done((log) => { .done((log) => {
setCiStatusFavicon(`${this.pageUrl}/status.json`); setCiStatusFavicon(`${this.pageUrl}/status.json`);
......
...@@ -191,7 +191,7 @@ export default { ...@@ -191,7 +191,7 @@ export default {
this.service.postAction(endpoint) this.service.postAction(endpoint)
.then(() => this.fetchEnvironments()) .then(() => this.fetchEnvironments())
.catch(() => new Flash('An error occured while making the request.')); .catch(() => new Flash('An error occurred while making the request.'));
} }
}, },
......
...@@ -170,7 +170,7 @@ export default { ...@@ -170,7 +170,7 @@ export default {
this.service.postAction(endpoint) this.service.postAction(endpoint)
.then(() => this.fetchEnvironments()) .then(() => this.fetchEnvironments())
.catch(() => new Flash('An error occured while making the request.')); .catch(() => new Flash('An error occurred while making the request.'));
} }
}, },
}, },
......
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
* causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a * causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a
*/ */
import Cookies from 'js-cookie';
const LINE_NUMBER_CLASS = 'diff-line-num'; const LINE_NUMBER_CLASS = 'diff-line-num';
const UNFOLDABLE_LINE_CLASS = 'js-unfold'; const UNFOLDABLE_LINE_CLASS = 'js-unfold';
const NO_COMMENT_CLASS = 'no-comment-btn'; const NO_COMMENT_CLASS = 'no-comment-btn';
...@@ -27,9 +29,7 @@ export default { ...@@ -27,9 +29,7 @@ export default {
this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === ''; this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === '';
} }
if (typeof notes !== 'undefined' && !this.isParallelView) { this.isParallelView = Cookies.get('diff_view') === 'parallel';
this.isParallelView = notes.isParallelView && notes.isParallelView();
}
if (this.userCanCreateNote) { if (this.userCanCreateNote) {
$diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e)) $diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e))
......
...@@ -14,7 +14,7 @@ class DropdownEmoji extends gl.FilteredSearchDropdown { ...@@ -14,7 +14,7 @@ class DropdownEmoji extends gl.FilteredSearchDropdown {
loadingTemplate: this.loadingTemplate, loadingTemplate: this.loadingTemplate,
onError() { onError() {
/* eslint-disable no-new */ /* eslint-disable no-new */
new Flash('An error occured fetching the dropdown data.'); new Flash('An error occurred fetching the dropdown data.');
/* eslint-enable no-new */ /* eslint-enable no-new */
}, },
}, },
......
...@@ -17,7 +17,7 @@ class DropdownNonUser extends gl.FilteredSearchDropdown { ...@@ -17,7 +17,7 @@ class DropdownNonUser extends gl.FilteredSearchDropdown {
preprocessing, preprocessing,
onError() { onError() {
/* eslint-disable no-new */ /* eslint-disable no-new */
new Flash('An error occured fetching the dropdown data.'); new Flash('An error occurred fetching the dropdown data.');
/* eslint-enable no-new */ /* eslint-enable no-new */
}, },
}, },
......
...@@ -27,7 +27,7 @@ class DropdownUser extends gl.FilteredSearchDropdown { ...@@ -27,7 +27,7 @@ class DropdownUser extends gl.FilteredSearchDropdown {
}, },
onError() { onError() {
/* eslint-disable no-new */ /* eslint-disable no-new */
new Flash('An error occured fetching the dropdown data.'); new Flash('An error occurred fetching the dropdown data.');
/* eslint-enable no-new */ /* eslint-enable no-new */
}, },
}, },
......
...@@ -44,7 +44,7 @@ class FilteredSearchManager { ...@@ -44,7 +44,7 @@ class FilteredSearchManager {
.catch((error) => { .catch((error) => {
if (error.name === 'RecentSearchesServiceError') return undefined; if (error.name === 'RecentSearchesServiceError') return undefined;
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new window.Flash('An error occured while parsing recent searches'); new window.Flash('An error occurred while parsing recent searches');
// Gracefully fail to empty array // Gracefully fail to empty array
return []; return [];
}) })
......
...@@ -16,9 +16,8 @@ const locales = allLocales.reduce((d, obj) => { ...@@ -16,9 +16,8 @@ const locales = allLocales.reduce((d, obj) => {
return data; return data;
}, {}); }, {});
let lang = document.querySelector('html').getAttribute('lang') || 'en'; const langAttribute = document.querySelector('html').getAttribute('lang');
lang = lang.replace(/-/g, '_'); const lang = (langAttribute || 'en').replace(/-/g, '_');
const locale = new Jed(locales[lang]); const locale = new Jed(locales[lang]);
/** /**
......
...@@ -311,7 +311,10 @@ $(function () { ...@@ -311,7 +311,10 @@ $(function () {
return $container.remove(); return $container.remove();
// Commit show suppressed diff // Commit show suppressed diff
}); });
$('.navbar-toggle').on('click', () => $('.header-content').toggleClass('menu-expanded')); $('.navbar-toggle').on('click', () => {
$('.header-content').toggleClass('menu-expanded');
gl.lazyLoader.loadCheck();
});
// Show/hide comments on diff // Show/hide comments on diff
$body.on('click', '.js-toggle-diff-comments', function (e) { $body.on('click', '.js-toggle-diff-comments', function (e) {
var $this = $(this); var $this = $(this);
......
...@@ -97,7 +97,7 @@ export default { ...@@ -97,7 +97,7 @@ export default {
postAction(endpoint) { postAction(endpoint) {
this.service.postAction(endpoint) this.service.postAction(endpoint)
.then(() => eventHub.$emit('refreshPipelines')) .then(() => eventHub.$emit('refreshPipelines'))
.catch(() => new Flash('An error occured while making the request.')); .catch(() => new Flash('An error occurred while making the request.'));
}, },
}, },
}; };
...@@ -73,7 +73,8 @@ import _ from 'underscore'; ...@@ -73,7 +73,8 @@ import _ from 'underscore';
aspectRatio: 1, aspectRatio: 1,
modal: true, modal: true,
scalable: false, scalable: false,
rotatable: false, rotatable: true,
checkOrientation: true,
zoomable: true, zoomable: true,
dragMode: 'move', dragMode: 'move',
guides: false, guides: false,
......
...@@ -37,14 +37,14 @@ export default { ...@@ -37,14 +37,14 @@ export default {
content: f.newContent, content: f.newContent,
})); }));
const payload = { const payload = {
branch: Store.targetBranch, branch: Store.currentBranch,
commit_message: commitMessage, commit_message: commitMessage,
actions, actions,
}; };
Store.submitCommitsLoading = true; Store.submitCommitsLoading = true;
Service.commitFiles(payload) Service.commitFiles(payload)
.then(this.resetCommitState) .then(this.resetCommitState)
.catch(() => Flash('An error occured while committing your changes')); .catch(() => Flash('An error occurred while committing your changes'));
}, },
resetCommitState() { resetCommitState() {
...@@ -105,7 +105,7 @@ export default { ...@@ -105,7 +105,7 @@ export default {
</label> </label>
<div class="col-md-6"> <div class="col-md-6">
<span class="help-block"> <span class="help-block">
{{targetBranch}} {{currentBranch}}
</span> </span>
</div> </div>
</div> </div>
......
...@@ -26,16 +26,6 @@ export default { ...@@ -26,16 +26,6 @@ export default {
this.editMode = !this.editMode; this.editMode = !this.editMode;
Store.toggleBlobView(); Store.toggleBlobView();
}, },
toggleProjectRefsForm() {
$('.project-refs-form').toggleClass('disabled', this.editMode);
$('.js-tree-ref-target-holder').toggle(this.editMode);
},
},
watch: {
editMode() {
this.toggleProjectRefsForm();
},
}, },
}; };
</script> </script>
......
...@@ -49,7 +49,7 @@ export default { ...@@ -49,7 +49,7 @@ export default {
v-else v-else
class="vertical-center render-error"> class="vertical-center render-error">
<p class="text-center"> <p class="text-center">
The source could not be displayed because a rendering error occured. You can <a :href="activeFile.raw_path">download</a> it instead. The source could not be displayed because a rendering error occurred. You can <a :href="activeFile.raw_path">download</a> it instead.
</p> </p>
</div> </div>
</div> </div>
......
...@@ -37,17 +37,24 @@ export default { ...@@ -37,17 +37,24 @@ export default {
let file = clickedFile; let file = clickedFile;
if (file.loading) return; if (file.loading) return;
file.loading = true; file.loading = true;
if (file.type === 'tree' && file.opened) { if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file); file = Store.removeChildFilesOfTree(file);
file.loading = false; file.loading = false;
} else { } else {
Service.url = file.url; const openFile = Helper.getFileFromPath(file.url);
Helper.getContent(file) if (openFile) {
.then(() => { file.loading = false;
file.loading = false; Store.setActiveFiles(openFile);
Helper.scrollTabsRight(); } else {
}) Service.url = file.url;
.catch(Helper.loadingError); Helper.getContent(file)
.then(() => {
file.loading = false;
Helper.scrollTabsRight();
})
.catch(Helper.loadingError);
}
} }
}, },
......
...@@ -263,6 +263,10 @@ const RepoHelper = { ...@@ -263,6 +263,10 @@ const RepoHelper = {
return Store.openedFiles.find(openedFile => Store.activeFile.url === openedFile.url); return Store.openedFiles.find(openedFile => Store.activeFile.url === openedFile.url);
}, },
getFileFromPath(path) {
return Store.openedFiles.find(file => file.url === path);
},
loadingError() { loadingError() {
Flash('Unable to load this content at this time.'); Flash('Unable to load this content at this time.');
}, },
......
...@@ -11,10 +11,6 @@ function initDropdowns() { ...@@ -11,10 +11,6 @@ function initDropdowns() {
} }
function addEventsForNonVueEls() { function addEventsForNonVueEls() {
$(document).on('change', '.dropdown', () => {
Store.targetBranch = $('.project-refs-target-form input[name="ref"]').val();
});
window.onbeforeunload = function confirmUnload(e) { window.onbeforeunload = function confirmUnload(e) {
const hasChanged = Store.openedFiles const hasChanged = Store.openedFiles
.some(file => file.changed); .some(file => file.changed);
......
...@@ -32,7 +32,6 @@ const RepoStore = { ...@@ -32,7 +32,6 @@ const RepoStore = {
isCommitable: false, isCommitable: false,
binary: false, binary: false,
currentBranch: '', currentBranch: '',
targetBranch: 'new-branch',
commitMessage: '', commitMessage: '',
binaryTypes: { binaryTypes: {
png: false, png: false,
......
...@@ -43,6 +43,8 @@ import Cookies from 'js-cookie'; ...@@ -43,6 +43,8 @@ import Cookies from 'js-cookie';
$allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right'); $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right');
$('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
$('.page-with-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); $('.page-with-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
if (gl.lazyLoader) gl.lazyLoader.loadCheck();
} }
if (!triggered) { if (!triggered) {
return Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed')); return Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
......
...@@ -38,7 +38,7 @@ class SidebarMoveIssue { ...@@ -38,7 +38,7 @@ class SidebarMoveIssue {
data: (searchTerm, callback) => { data: (searchTerm, callback) => {
this.mediator.fetchAutocompleteProjects(searchTerm) this.mediator.fetchAutocompleteProjects(searchTerm)
.then(callback) .then(callback)
.catch(() => new Flash('An error occured while fetching projects autocomplete.')); .catch(() => new Flash('An error occurred while fetching projects autocomplete.'));
}, },
renderRow: project => ` renderRow: project => `
<li> <li>
...@@ -73,7 +73,7 @@ class SidebarMoveIssue { ...@@ -73,7 +73,7 @@ class SidebarMoveIssue {
this.mediator.moveIssue() this.mediator.moveIssue()
.catch(() => { .catch(() => {
Flash('An error occured while moving the issue.'); Flash('An error occurred while moving the issue.');
this.$confirmButton this.$confirmButton
.enable() .enable()
.removeClass('is-loading'); .removeClass('is-loading');
......
...@@ -41,7 +41,7 @@ export default class SidebarMediator { ...@@ -41,7 +41,7 @@ export default class SidebarMediator {
this.store.setAssigneeData(data); this.store.setAssigneeData(data);
this.store.setTimeTrackingData(data); this.store.setTimeTrackingData(data);
}) })
.catch(() => new Flash('Error occured when fetching sidebar data')); .catch(() => new Flash('Error occurred when fetching sidebar data'));
} }
fetchAutocompleteProjects(searchTerm) { fetchAutocompleteProjects(searchTerm) {
......
...@@ -535,7 +535,6 @@ ...@@ -535,7 +535,6 @@
} }
.diff-notes-collapse { .diff-notes-collapse {
position: relative;
width: 19px; width: 19px;
height: 19px; height: 19px;
padding: 0; padding: 0;
...@@ -543,11 +542,7 @@ ...@@ -543,11 +542,7 @@
z-index: 100; z-index: 100;
svg { svg {
position: absolute; vertical-align: text-top;
left: 50%;
top: 50%;
margin-left: -5.5px;
margin-top: -5.5px;
} }
path { path {
......
module CustomAttributesFilter
def by_custom_attributes(items)
return items unless params[:custom_attributes].is_a?(Hash)
return items unless Ability.allowed?(current_user, :read_custom_attribute)
association = items.reflect_on_association(:custom_attributes)
attributes_table = association.klass.arel_table
attributable_table = items.model.arel_table
custom_attributes = association.klass.select('true').where(
attributes_table[association.foreign_key]
.eq(attributable_table[association.association_primary_key])
)
# perform a subquery for each attribute to be filtered
params[:custom_attributes].inject(items) do |scope, (key, value)|
scope.where('EXISTS (?)', custom_attributes.where(key: key, value: value))
end
end
end
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
# #
class UsersFinder class UsersFinder
include CreatedAtFilter include CreatedAtFilter
include CustomAttributesFilter
attr_accessor :current_user, :params attr_accessor :current_user, :params
...@@ -33,6 +34,7 @@ class UsersFinder ...@@ -33,6 +34,7 @@ class UsersFinder
users = by_external_identity(users) users = by_external_identity(users)
users = by_external(users) users = by_external(users)
users = by_created_at(users) users = by_created_at(users)
users = by_custom_attributes(users)
users = by_non_ldap(users) users = by_non_ldap(users)
users users
......
...@@ -10,11 +10,7 @@ module BreadcrumbsHelper ...@@ -10,11 +10,7 @@ module BreadcrumbsHelper
def breadcrumb_title_link def breadcrumb_title_link
return @breadcrumb_link if @breadcrumb_link return @breadcrumb_link if @breadcrumb_link
if controller.available_action?(:index) request.path
url_for(action: "index")
else
request.path
end
end end
def breadcrumb_title(title) def breadcrumb_title(title)
......
...@@ -249,16 +249,25 @@ module IssuablesHelper ...@@ -249,16 +249,25 @@ module IssuablesHelper
Gitlab::IssuablesCountForState.new(finder)[state] Gitlab::IssuablesCountForState.new(finder)[state]
end end
def close_issuable_url(issuable) def close_issuable_path(issuable)
issuable_url(issuable, close_reopen_params(issuable, :close)) issuable_path(issuable, close_reopen_params(issuable, :close))
end end
def reopen_issuable_url(issuable) def reopen_issuable_path(issuable)
issuable_url(issuable, close_reopen_params(issuable, :reopen)) issuable_path(issuable, close_reopen_params(issuable, :reopen))
end end
def close_reopen_issuable_url(issuable, should_inverse = false) def close_reopen_issuable_path(issuable, should_inverse = false)
issuable.closed? ^ should_inverse ? reopen_issuable_url(issuable) : close_issuable_url(issuable) issuable.closed? ^ should_inverse ? reopen_issuable_path(issuable) : close_issuable_path(issuable)
end
def issuable_path(issuable, *options)
case issuable
when Issue
issue_path(issuable, *options)
when MergeRequest
merge_request_path(issuable, *options)
end
end end
def issuable_url(issuable, *options) def issuable_url(issuable, *options)
......
...@@ -496,13 +496,7 @@ class Repository ...@@ -496,13 +496,7 @@ class Repository
def exists? def exists?
return false unless full_path return false unless full_path
Gitlab::GitalyClient.migrate(:repository_exists) do |enabled| raw_repository.exists?
if enabled
raw_repository.exists?
else
refs_directory_exists?
end
end
end end
cache_method :exists? cache_method :exists?
...@@ -1143,12 +1137,6 @@ class Repository ...@@ -1143,12 +1137,6 @@ class Repository
blob.data blob.data
end end
def refs_directory_exists?
circuit_breaker.perform do
File.exist?(File.join(path_to_repo, 'refs'))
end
end
def cache def cache
# TODO: should we use UUIDs here? We could move repositories without clearing this cache # TODO: should we use UUIDs here? We could move repositories without clearing this cache
@cache ||= RepositoryCache.new(full_path, @project.id) @cache ||= RepositoryCache.new(full_path, @project.id)
...@@ -1200,10 +1188,6 @@ class Repository ...@@ -1200,10 +1188,6 @@ class Repository
Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, false)) Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, false))
end end
def circuit_breaker
@circuit_breaker ||= Gitlab::Git::Storage::CircuitBreaker.for_storage(project.repository_storage)
end
def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset) def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
ref ||= root_ref ref ||= root_ref
......
...@@ -133,6 +133,8 @@ class User < ActiveRecord::Base ...@@ -133,6 +133,8 @@ class User < ActiveRecord::Base
has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent
has_many :custom_attributes, class_name: 'UserCustomAttribute'
# #
# Validations # Validations
# #
......
class UserCustomAttribute < ActiveRecord::Base
belongs_to :user
validates :user_id, :key, :value, presence: true
validates :key, uniqueness: { scope: [:user_id] }
end
...@@ -47,4 +47,9 @@ class GlobalPolicy < BasePolicy ...@@ -47,4 +47,9 @@ class GlobalPolicy < BasePolicy
rule { ~(anonymous & restricted_public_level) }.policy do rule { ~(anonymous & restricted_public_level) }.policy do
enable :read_users_list enable :read_users_list
end end
rule { admin }.policy do
enable :read_custom_attribute
enable :update_custom_attribute
end
end end
...@@ -14,6 +14,7 @@ module MergeRequests ...@@ -14,6 +14,7 @@ module MergeRequests
notification_service.merge_mr(merge_request, current_user) notification_service.merge_mr(merge_request, current_user)
execute_hooks(merge_request, 'merge') execute_hooks(merge_request, 'merge')
invalidate_cache_counts(merge_request, users: merge_request.assignees) invalidate_cache_counts(merge_request, users: merge_request.assignees)
merge_request.update_project_counter_caches
end end
private private
......
...@@ -11,7 +11,7 @@ module Tags ...@@ -11,7 +11,7 @@ module Tags
begin begin
new_tag = repository.add_tag(current_user, tag_name, target, message) new_tag = repository.add_tag(current_user, tag_name, target, message)
rescue Rugged::TagError rescue Gitlab::Git::Repository::TagExistsError
return error("Tag #{tag_name} already exists") return error("Tag #{tag_name} already exists")
rescue Gitlab::Git::HooksService::PreReceiveError => ex rescue Gitlab::Git::HooksService::PreReceiveError => ex
return error(ex.message) return error(ex.message)
......
...@@ -12,10 +12,10 @@ module Users ...@@ -12,10 +12,10 @@ module Users
def execute(validate: true, &block) def execute(validate: true, &block)
yield(@user) if block_given? yield(@user) if block_given?
assign_attributes(&block)
user_exists = @user.persisted? user_exists = @user.persisted?
assign_attributes(&block)
if @user.save(validate: validate) if @user.save(validate: validate)
notify_success(user_exists) notify_success(user_exists)
else else
...@@ -31,7 +31,7 @@ module Users ...@@ -31,7 +31,7 @@ module Users
true true
end end
protected private
def notify_success(user_exists) def notify_success(user_exists)
notify_new_user(@user, nil) unless user_exists notify_new_user(@user, nil) unless user_exists
...@@ -39,8 +39,6 @@ module Users ...@@ -39,8 +39,6 @@ module Users
success success
end end
private
def assign_attributes(&block) def assign_attributes(&block)
if @user.user_synced_attributes_metadata if @user.user_synced_attributes_metadata
params.except!(*@user.user_synced_attributes_metadata.read_only_attributes) params.except!(*@user.user_synced_attributes_metadata.read_only_attributes)
......
- confirmation_link = confirmation_url(@resource, confirmation_token: @token)
- if @resource.unconfirmed_email.present? - if @resource.unconfirmed_email.present?
#content #content
= email_default_heading(@resource.unconfirmed_email) = email_default_heading(@resource.unconfirmed_email)
%p Click the link below to confirm your email address. %p Click the link below to confirm your email address.
#cta #cta
= link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token) = link_to confirmation_link, confirmation_link
- else - else
#content #content
- if Gitlab.com? - if Gitlab.com?
...@@ -12,4 +13,4 @@ ...@@ -12,4 +13,4 @@
= email_default_heading("Welcome, #{@resource.name}!") = email_default_heading("Welcome, #{@resource.name}!")
%p To get started, click the link below to confirm your account. %p To get started, click the link below to confirm your account.
#cta #cta
= link_to 'Confirm your account', confirmation_url(@resource, confirmation_token: @token) = link_to confirmation_link, confirmation_link
...@@ -26,19 +26,19 @@ ...@@ -26,19 +26,19 @@
= link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('search') = icon('search')
- if current_user - if current_user
%li.user-counter = nav_link(path: 'dashboard#issues', html_options: { class: "user-counter" }) do
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('issues') = custom_icon('issues')
- issues_count = assigned_issuables_count(:issues) - issues_count = assigned_issuables_count(:issues)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) } %span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count) = number_with_delimiter(issues_count)
%li.user-counter = nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter" }) do
= link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to assigned_mrs_dashboard_path, title: 'Merge requests', class: 'dashboard-shortcuts-merge_requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('mr_bold') = custom_icon('mr_bold')
- merge_requests_count = assigned_issuables_count(:merge_requests) - merge_requests_count = assigned_issuables_count(:merge_requests)
%span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) } %span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= number_with_delimiter(merge_requests_count) = number_with_delimiter(merge_requests_count)
%li.user-counter = nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do
= link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('todo_done') = custom_icon('todo_done')
%span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) } %span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
......
...@@ -16,5 +16,5 @@ ...@@ -16,5 +16,5 @@
= breadcrumb_list_item link_to(extra[:text], extra[:link]) = breadcrumb_list_item link_to(extra[:text], extra[:link])
= render "layouts/nav/breadcrumbs/collapsed_dropdown", location: :after = render "layouts/nav/breadcrumbs/collapsed_dropdown", location: :after
%li %li
%h2.breadcrumbs-sub-title= @breadcrumb_title %h2.breadcrumbs-sub-title= link_to @breadcrumb_title, breadcrumb_title_link
= yield :header_content = yield :header_content
- access = note_max_access_for_user(note) - access = note_max_access_for_user(note)
- if note.has_special_role?(Note::SpecialRole::FIRST_TIME_CONTRIBUTOR) - if note.has_special_role?(Note::SpecialRole::FIRST_TIME_CONTRIBUTOR)
%span.note-role.note-role-special.has-tooltip{ title: _("This is the author's first Merge Request to this project. Handle with care.") } %span.note-role.note-role-special.has-tooltip{ title: _("This is the author's first Merge Request to this project.") }
= issuable_first_contribution_icon = issuable_first_contribution_icon
- if access.nonzero? - if access.nonzero?
%span.note-role.note-role-access= Gitlab::Access.human_access(access) %span.note-role.note-role-access= Gitlab::Access.human_access(access)
......
.tree-ref-container .tree-ref-container
.tree-ref-holder .tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path = render 'shared/ref_switcher', destination: 'tree', path: @path
- if show_new_repo?
.tree-ref-target-holder.js-tree-ref-target-holder
= icon('long-arrow-right', title: 'to target branch')
= render 'shared/target_switcher', destination: 'tree', path: @path
- unless show_new_repo? - unless show_new_repo?
= render 'projects/tree/old_tree_header' = render 'projects/tree/old_tree_header'
......
- dropdown_toggle_text = @ref || @project.default_branch
= form_tag nil, method: :get, class: "project-refs-form project-refs-target-form" do
= hidden_field_tag :destination, destination
- if defined?(path)
= hidden_field_tag :path, path
- @options && @options.each do |key, value|
= hidden_field_tag key, value, id: nil
.dropdown
= dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project, find: ['branches']), field_name: 'ref', input_field_name: 'new-branch', submit_form_on_click: true, visit: false }, { toggle_class: "js-project-refs-dropdown" }
.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
= dropdown_title _("Create a new branch")
= dropdown_input _("Create a new branch")
= dropdown_title _("Select existing branch"), options: {close: false}
= dropdown_filter _("Search branches and tags")
= dropdown_content
= dropdown_loading
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
- button_method = issuable_close_reopen_button_method(issuable) - button_method = issuable_close_reopen_button_method(issuable)
- if can_update && is_current_user - if can_update && is_current_user
= link_to "Close #{display_issuable_type}", close_issuable_url(issuable), method: button_method, = link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method,
class: "hidden-xs hidden-sm btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}" class: "hidden-xs hidden-sm btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}"
= link_to "Reopen #{display_issuable_type}", reopen_issuable_url(issuable), method: button_method, = link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method,
class: "hidden-xs hidden-sm btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}" class: "hidden-xs hidden-sm btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
- elsif can_update && !is_current_user - elsif can_update && !is_current_user
= render 'shared/issuable/close_reopen_report_toggle', issuable: issuable = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
- button_method = issuable_close_reopen_button_method(issuable) - button_method = issuable_close_reopen_button_method(issuable)
.pull-left.btn-group.prepend-left-10.issuable-close-dropdown.droplab-dropdown.js-issuable-close-dropdown .pull-left.btn-group.prepend-left-10.issuable-close-dropdown.droplab-dropdown.js-issuable-close-dropdown
= link_to "#{display_button_action} #{display_issuable_type}", close_reopen_issuable_url(issuable), = link_to "#{display_button_action} #{display_issuable_type}", close_reopen_issuable_path(issuable),
method: button_method, class: "#{button_class} btn-#{button_action}", title: "#{display_button_action} #{display_issuable_type}" method: button_method, class: "#{button_class} btn-#{button_action}", title: "#{display_button_action} #{display_issuable_type}"
= button_tag type: 'button', class: "#{toggle_class} btn-#{button_action}-color", = button_tag type: 'button', class: "#{toggle_class} btn-#{button_action}-color",
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
%ul#issuable-close-menu.js-issuable-close-menu.dropdown-menu{ class: button_responsive_class, data: { dropdown: true } } %ul#issuable-close-menu.js-issuable-close-menu.dropdown-menu{ class: button_responsive_class, data: { dropdown: true } }
%li.close-item{ class: "#{issuable_button_visibility(issuable, true) || 'droplab-item-selected'}", %li.close-item{ class: "#{issuable_button_visibility(issuable, true) || 'droplab-item-selected'}",
data: { text: "Close #{display_issuable_type}", url: close_issuable_url(issuable), data: { text: "Close #{display_issuable_type}", url: close_issuable_path(issuable),
button_class: "#{button_class} btn-close", toggle_class: "#{toggle_class} btn-close-color", method: button_method } } button_class: "#{button_class} btn-close", toggle_class: "#{toggle_class} btn-close-color", method: button_method } }
%button.btn.btn-transparent %button.btn.btn-transparent
= icon('check', class: 'icon') = icon('check', class: 'icon')
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
= display_issuable_type = display_issuable_type
%li.reopen-item{ class: "#{issuable_button_visibility(issuable, false) || 'droplab-item-selected'}", %li.reopen-item{ class: "#{issuable_button_visibility(issuable, false) || 'droplab-item-selected'}",
data: { text: "Reopen #{display_issuable_type}", url: reopen_issuable_url(issuable), data: { text: "Reopen #{display_issuable_type}", url: reopen_issuable_path(issuable),
button_class: "#{button_class} btn-reopen", toggle_class: "#{toggle_class} btn-reopen-color", method: button_method } } button_class: "#{button_class} btn-reopen", toggle_class: "#{toggle_class} btn-reopen-color", method: button_method } }
%button.btn.btn-transparent %button.btn.btn-transparent
= icon('check', class: 'icon') = icon('check', class: 'icon')
......
---
title: Confirmation email shows link as text instead of human readable text
merge_request: 14243
author: bitsapien
type: changed
---
title: Add active states to nav bar counters
merge_request:
author:
type: changed
---
title: Fix 500 error on merged merge requests when GitLab is restored from a backup
merge_request:
author:
type: fixed
---
title: Fix notes type created from import. This should fix some missing notes issues
from imported projects
merge_request: 14524
author:
type: fixed
---
title: Adjust MRs being stuck on "process of being merged" for more than 2 hours
merge_request:
author:
type: fixed
---
title: Fixes data parameter not being sent in ajax request for jobs log
merge_request:
author:
type: fixed
---
title: Add index for merge_requests.merge_commit_sha
merge_request:
author:
type: other
---
title: Fixed issue/merge request breadcrumb titles not having links
merge_request:
author:
type: fixed
---
title: Fix CSRF validation issue when closing/opening merge requests from the UI
merge_request: 14555
author:
type: fixed
---
title: Fixed commenting on side-by-side commit diff
merge_request:
author:
type: fixed
---
title: Make sure API responds with 401 when invalid authentication info is provided
merge_request:
author:
type: fixed
---
title: Clarify artifact download via the API only accepts branch or tag name for ref
merge_request:
author:
type: other
---
title: Change recommended MySQL version to 5.6
merge_request:
author:
type: other
---
title: Support custom attributes on users
merge_request: 13038
author: Markus Koller
---
title: Fix merge request counter updates after merge
merge_request:
author:
type: fixed
---
title: Fix profile image orientation based on EXIF data gvieira37
merge_request: 14461
author: gvieira37
type: fixed
---
title: Gitaly RepositoryExists remains opt-in for all method calls
merge_request:
author:
type: fixed
class CreateUserCustomAttributes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :user_custom_attributes do |t|
t.timestamps_with_timezone null: false
t.references :user, null: false, foreign_key: { on_delete: :cascade }
t.string :key, null: false
t.string :value, null: false
t.index [:user_id, :key], unique: true
t.index [:key, :value]
end
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddCompositeIndexOnMergeRequestsMergeCommitSha < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# The default index name is too long for PostgreSQL and would thus be
# truncated.
INDEX_NAME = 'index_merge_requests_on_tp_id_and_merge_commit_sha_and_id'
COLUMNS = [:target_project_id, :merge_commit_sha, :id]
disable_ddl_transaction!
def up
return if index_is_present?
add_concurrent_index(:merge_requests, COLUMNS, name: INDEX_NAME)
end
def down
return unless index_is_present?
remove_concurrent_index(:merge_requests, COLUMNS, name: INDEX_NAME)
end
def index_is_present?
index_exists?(:merge_requests, COLUMNS, name: INDEX_NAME)
end
end
class UpdateLegacyDiffNotesTypeForImport < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
update_column_in_batches(:notes, :type, 'LegacyDiffNote') do |table, query|
query.where(table[:type].eq('Github::Import::LegacyDiffNote'))
end
end
def down
end
end
class UpdateNotesTypeForImport < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
update_column_in_batches(:notes, :type, 'Note') do |table, query|
query.where(table[:type].eq('Github::Import::Note'))
end
end
def down
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170921115009) do ActiveRecord::Schema.define(version: 20170928100231) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -1123,6 +1123,7 @@ ActiveRecord::Schema.define(version: 20170921115009) do ...@@ -1123,6 +1123,7 @@ ActiveRecord::Schema.define(version: 20170921115009) do
add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree
add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree
add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree
add_index "merge_requests", ["target_project_id", "merge_commit_sha", "id"], name: "index_merge_requests_on_tp_id_and_merge_commit_sha_and_id", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"} add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
...@@ -1890,6 +1891,17 @@ ActiveRecord::Schema.define(version: 20170921115009) do ...@@ -1890,6 +1891,17 @@ ActiveRecord::Schema.define(version: 20170921115009) do
add_index "user_agent_details", ["subject_id", "subject_type"], name: "index_user_agent_details_on_subject_id_and_subject_type", using: :btree add_index "user_agent_details", ["subject_id", "subject_type"], name: "index_user_agent_details_on_subject_id_and_subject_type", using: :btree
create_table "user_custom_attributes", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "user_id", null: false
t.string "key", null: false
t.string "value", null: false
end
add_index "user_custom_attributes", ["key", "value"], name: "index_user_custom_attributes_on_key_and_value", using: :btree
add_index "user_custom_attributes", ["user_id", "key"], name: "index_user_custom_attributes_on_user_id_and_key", unique: true, using: :btree
create_table "user_synced_attributes_metadata", force: :cascade do |t| create_table "user_synced_attributes_metadata", force: :cascade do |t|
t.boolean "name_synced", default: false t.boolean "name_synced", default: false
t.boolean "email_synced", default: false t.boolean "email_synced", default: false
...@@ -2160,6 +2172,7 @@ ActiveRecord::Schema.define(version: 20170921115009) do ...@@ -2160,6 +2172,7 @@ ActiveRecord::Schema.define(version: 20170921115009) do
add_foreign_key "todos", "projects", name: "fk_45054f9c45", on_delete: :cascade add_foreign_key "todos", "projects", name: "fk_45054f9c45", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users" add_foreign_key "u2f_registrations", "users"
add_foreign_key "user_custom_attributes", "users", on_delete: :cascade
add_foreign_key "user_synced_attributes_metadata", "users", on_delete: :cascade add_foreign_key "user_synced_attributes_metadata", "users", on_delete: :cascade
add_foreign_key "users_star_projects", "projects", name: "fk_22cd27ddfc", on_delete: :cascade add_foreign_key "users_star_projects", "projects", name: "fk_22cd27ddfc", on_delete: :cascade
add_foreign_key "web_hook_logs", "web_hooks", on_delete: :cascade add_foreign_key "web_hook_logs", "web_hooks", on_delete: :cascade
......
...@@ -14,6 +14,7 @@ following locations: ...@@ -14,6 +14,7 @@ following locations:
- [Project-level Variables](project_level_variables.md) - [Project-level Variables](project_level_variables.md)
- [Group-level Variables](group_level_variables.md) - [Group-level Variables](group_level_variables.md)
- [Commits](commits.md) - [Commits](commits.md)
- [Custom Attributes](custom_attributes.md)
- [Deployments](deployments.md) - [Deployments](deployments.md)
- [Deploy Keys](deploy_keys.md) - [Deploy Keys](deploy_keys.md)
- [Environments](environments.md) - [Environments](environments.md)
......
# Custom Attributes API
Every API call to custom attributes must be authenticated as administrator.
## List custom attributes
Get all custom attributes on a user.
```
GET /users/:id/custom_attributes
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a user |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/42/custom_attributes
```
Example response:
```json
[
{
"key": "location",
"value": "Antarctica"
},
{
"key": "role",
"value": "Developer"
}
]
```
## Single custom attribute
Get a single custom attribute on a user.
```
GET /users/:id/custom_attributes/:key
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a user |
| `key` | string | yes | The key of the custom attribute |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/42/custom_attributes/location
```
Example response:
```json
{
"key": "location",
"value": "Antarctica"
}
```
## Set custom attribute
Set a custom attribute on a user. The attribute will be updated if it already exists,
or newly created otherwise.
```
PUT /users/:id/custom_attributes/:key
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a user |
| `key` | string | yes | The key of the custom attribute |
| `value` | string | yes | The value of the custom attribute |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "value=Greenland" https://gitlab.example.com/api/v4/users/42/custom_attributes/location
```
Example response:
```json
{
"key": "location",
"value": "Greenland"
}
```
## Delete custom attribute
Delete a custom attribute on a user.
```
DELETE /users/:id/custom_attributes/:key
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a user |
| `key` | string | yes | The key of the custom attribute |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/42/custom_attributes/location
```
...@@ -359,7 +359,7 @@ Parameters ...@@ -359,7 +359,7 @@ Parameters
| Attribute | Type | Required | Description | | Attribute | Type | Required | Description |
|-------------|---------|----------|-------------------------- | |-------------|---------|----------|-------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `ref_name` | string | yes | The ref from a repository | | `ref_name` | string | yes | The ref from a repository (can only be branch or tag name, not HEAD or SHA) |
| `job` | string | yes | The name of the job | | `job` | string | yes | The name of the job |
| `job_token` | string | no | To be used with [triggers] for multi-project pipelines. Is should be invoked only inside `.gitlab-ci.yml`. Its value is always `$CI_JOB_TOKEN`. | | `job_token` | string | no | To be used with [triggers] for multi-project pipelines. Is should be invoked only inside `.gitlab-ci.yml`. Its value is always `$CI_JOB_TOKEN`. |
......
...@@ -156,6 +156,12 @@ You can search users by creation date time range with: ...@@ -156,6 +156,12 @@ You can search users by creation date time range with:
GET /users?created_before=2001-01-02T00:00:00.060Z&created_after=1999-01-02T00:00:00.060 GET /users?created_before=2001-01-02T00:00:00.060Z&created_after=1999-01-02T00:00:00.060
``` ```
You can filter by [custom attributes](custom_attributes.md) with:
```
GET /users?custom_attributes[key]=value&custom_attributes[other_key]=other_value
```
## Single user ## Single user
Get a single user. Get a single user.
......
...@@ -250,6 +250,8 @@ By default, when using `docker:dind`, Docker uses the `vfs` storage driver which ...@@ -250,6 +250,8 @@ By default, when using `docker:dind`, Docker uses the `vfs` storage driver which
copies the filesystem on every run. This is a very disk-intensive operation copies the filesystem on every run. This is a very disk-intensive operation
which can be avoided if a different driver is used, for example `overlay2`. which can be avoided if a different driver is used, for example `overlay2`.
### Requirements
1. Make sure a recent kernel is used, preferably `>= 4.2`. 1. Make sure a recent kernel is used, preferably `>= 4.2`.
1. Check whether the `overlay` module is loaded: 1. Check whether the `overlay` module is loaded:
...@@ -271,14 +273,27 @@ which can be avoided if a different driver is used, for example `overlay2`. ...@@ -271,14 +273,27 @@ which can be avoided if a different driver is used, for example `overlay2`.
overlay overlay
``` ```
1. Use the driver by defining a variable at the top of your `.gitlab-ci.yml`: ### Use driver per project
``` You can enable the driver for each project individually by editing the project's `.gitlab-ci.yml`:
variables:
DOCKER_DRIVER: overlay2 ```
``` variables:
DOCKER_DRIVER: overlay2
> **Note:** ```
### Use driver for every project
To enable the driver for every project, you can set the environment variable for every build by adding `environment` in the `[[runners]]` section of `config.toml`:
```toml
environment = ["DOCKER_DRIVER=overlay2"]
```
If you're running multiple Runners you will have to modify all configuration files.
> **Notes:**
- More information about the Runner configuration is available in the [Runner documentation](https://docs.gitlab.com/runner/configuration/).
- For more information about using OverlayFS with Docker, you can read - For more information about using OverlayFS with Docker, you can read
[Use the OverlayFS storage driver](https://docs.docker.com/engine/userguide/storagedriver/overlayfs-driver/). [Use the OverlayFS storage driver](https://docs.docker.com/engine/userguide/storagedriver/overlayfs-driver/).
......
...@@ -154,6 +154,16 @@ always in-sync with the codebase. ...@@ -154,6 +154,16 @@ always in-sync with the codebase.
EE-specific tests follows the same organization, but under the `spec/ee` folder. EE-specific tests follows the same organization, but under the `spec/ee` folder.
## Test for what should not be there
This is particularly important for permission calls and might be called a
negative assertion: make sure only the bare minimum is returned and nothing else.
See an issue about [leaking tokens] as an example of a vulnerability that is
captured by such a test.
[leaking tokens]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37948
## How to test at the correct level? ## How to test at the correct level?
As many things in life, deciding what to test at each level of testing is a As many things in life, deciding what to test at each level of testing is a
......
# Illustrations
The illustrations should always align with topics and goals in specific context.
## Principles
#### Be simple.
- For clarity, we use simple and specific elements to create our illustrations.
#### Be optimistic.
- We are an open-minded, optimistic, and friendly team. We should reflect those values in our illustrations to connect with our brand experience.
#### Be gentle.
- Our illustrations assist users in understanding context and guide users in the right direction. Illustrations are supportive, so they should be obvious but not aggressive.
## Style
#### Shapes
- All illustrations are geometric rather than organic.
- The illustrations are made by circles, rectangles, squares, and triangles.
<img src="img/illustrations-geometric.png" width=224px alt="Example for geometric" />
#### Stroke
- Standard border thickness: **4px**
- Depending on the situation, border thickness can be changed to **3px**. For example, when the illustration size is small, an illustration with 4px border thickness would look tight. In this case, the border thickness can be changed to 3px.
- We use **rounded caps** and **rounded corner**.
| Do | Don't |
| -------- | -------- |
| <img src="img/illustrations-caps-do.png" width= 133px alt="Do: caps and corner" /> | <img src="img/illustrations-caps-don't.png" width= 133px alt="Don't: caps and corner"/> |
#### Radius
- Standard corner radius: **10px**
- Depending on the situation, corner radius can be changed to **5px**. For example, when the illustration size is small, an illustration with 10px corner radius would be over-rounded. In this case, the corner radius can be changed to 5px.
<img src="img/illustrations-border-radius.png" width= 464px alt="Example for border radius"/>
#### Size
Depends on the situation, the illustration size can be the 3 types below:
**Large**
* Use case: Empty states, error pages(e.g. 404, 403)
* For vertical layout, the illustration should not larger than **430*300 px**.
* For horizontal layout, the illustration should not larger than **430*380 px**.
| Vertical layout | Horizontal layout |
| --------------- | ----------------- |
| <img src="img/illustration-size-large-vertical.png" /> | <img src="img/illustration-size-large-horizontal.png" />
**Medium**
* Use case: Banner
* The illustration should not larger than **240*160 px**
* The illustration should keep simple and clear. We recommend not including too many elements in the medium size illustration.
<img src="img/illustration-size-medium.png" width=983px />
**Small**
* Use case: Graphics for explanatory text, graphics for status.
* The illustration should not larger than **160*90 px**.
* The illustration should keep simple and clear. We recommend not including too many elements in the small size illustration.
<img src="img/illustration-size-small.png" width=983px />
**Illustration on mobile**
- Keep the proportions in original ratio.
#### Colors palette
For consistency, we recommend choosing colors from our color palette.
| Orange | Purple | Grey |
| -------- | -------- | -------- |
| <img src="img/illustrations-color-orange.png" width= 160px alt="Orange" /> | <img src="img/illustrations-color-purple.png" width= 160px alt="Purple" /> | <img src="img/illustrations-color-grey.png" width= 160px alt="Grey" /> |
| #FC6D26 | #6B4FBB | #EEEEEE |
#### Don't
- Don't include the typography in the illustration.
- Don't include tanuki in the illustration. If necessary, we recommend having tanuki monochromatic.
---
| Orange | Purple |
| -------- | -------- |
| <img src="img/illustrations-palette-oragne.png" width= 160px alt="Palette - Orange" /> | <img src="img/illustrations-palette-purple.png" width= 160px alt="Palette - Purple" /> |
...@@ -21,6 +21,11 @@ Guidance on the timing, curving and motion for GitLab. ...@@ -21,6 +21,11 @@ Guidance on the timing, curving and motion for GitLab.
--- ---
### [Illustrations](illustrations.md)
Guidelines for principals and styles related to illustrations for GitLab.
---
### [Copy](copy.md) ### [Copy](copy.md)
Conventions on text and messaging within labels, buttons, and other components. Conventions on text and messaging within labels, buttons, and other components.
......
# Writing documentation # Writing documentation
- **General Documentation**: written by the developers responsible by creating features. Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers. - **General Documentation**: written by the [developers responsible by creating features](#contributing-to-docs). Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers.
- **Technical Articles**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/). - **Technical Articles**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/).
- **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs in the same merge request containing code. They gather all resources for that topic in a single page (user and admin documentation, articles, and third-party docs). - **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs in the same merge request containing code. They gather all resources for that topic in a single page (user and admin documentation, articles, and third-party docs).
...@@ -69,6 +69,51 @@ Use the [writing method](https://about.gitlab.com/handbook/product/technical-wri ...@@ -69,6 +69,51 @@ Use the [writing method](https://about.gitlab.com/handbook/product/technical-wri
All the docs follow the same [styleguide](doc_styleguide.md). All the docs follow the same [styleguide](doc_styleguide.md).
### Contributing to docs
Whenever a feature is changed, updated, introduced, or deprecated, the merge
request introducing these changes must be accompanied by the documentation
(either updating existing ones or creating new ones). This is also valid when
changes are introduced to the UI.
The one resposible for writing the first piece of documentation is the developer who
wrote the code. It's the job of the Product Manager to ensure all features are
shipped with its docs, whether is a small or big change. At the pace GitLab evolves,
this is the only way to keep the docs up-to-date. If you have any questions about it,
please ask a Technical Writer. Otherwise, when your content is ready, assign one of
them to review it for you.
We use the [monthly release blog post](https://about.gitlab.com/handbook/marketing/blog/release-posts/#monthly-releases) as a changelog checklist to ensure everything
is documented.
### Feature overview and use cases
Every major feature (regardless if present in GitLab Community or Enterprise editions)
should present, at the beginning of the document, two main sections: **overview** and
**use cases**. Every GitLab EE-only feature should also contain these sections.
**Overview**: at the name suggests, the goal here is to provide an overview of the feature.
Describe what is it, what it does, why it is important/cool/nice-to-have,
what problem it solves, and what you can do with this feature that you couldn't
do before.
**Use cases**: provide at least two, ideally three, use cases for every major feature.
You should answer this question: what can you do with this feature/change? Use cases
are examples of how this feauture or change can be used in real life.
Examples:
- CE and EE: [Issues](../user/project/issues/index.md#use-cases)
- CE and EE: [Merge Requests](../user/project/merge_requests/index.md#overview)
- EE-only: [Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html#overview)
- EE-only: [Jenkins integration](https://docs.gitlab.com/ee/integration/jenkins.md#overview)
Note that if you don't have anything to add between the doc title (`<h1>`) and
the header `## Overview`, you can omit the header, but keep the content of the
overview there.
> **Overview** and **use cases** are required to **every** Enterprise Edition feature,
and for every **major** feature present in Community Edition.
### Markdown ### Markdown
Currently GitLab docs use Redcarpet as [markdown](../user/markdown.md) engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future. Currently GitLab docs use Redcarpet as [markdown](../user/markdown.md) engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future.
......
# Database MySQL # Database MySQL
>**Note:** >**Note:**
We do not recommend using MySQL due to various issues. For example, case - We do not recommend using MySQL due to various issues. For example, case
[(in)sensitivity](https://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html) [(in)sensitivity](https://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html)
and [problems](https://bugs.mysql.com/bug.php?id=65830) that and [problems](https://bugs.mysql.com/bug.php?id=65830) that
[suggested](https://bugs.mysql.com/bug.php?id=50909) [suggested](https://bugs.mysql.com/bug.php?id=50909)
[fixes](https://bugs.mysql.com/bug.php?id=65830) [have](https://bugs.mysql.com/bug.php?id=63164). [fixes](https://bugs.mysql.com/bug.php?id=65830) [have](https://bugs.mysql.com/bug.php?id=63164).
- We recommend using MySQL version 5.6 or later. Please see the following [issue][ce-38152].
## Initial database setup ## Initial database setup
...@@ -13,7 +14,7 @@ and [problems](https://bugs.mysql.com/bug.php?id=65830) that ...@@ -13,7 +14,7 @@ and [problems](https://bugs.mysql.com/bug.php?id=65830) that
# Install the database packages # Install the database packages
sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev
# Ensure you have MySQL version 5.5.14 or later # Ensure you have MySQL version 5.6 or later
mysql --version mysql --version
# Pick a MySQL root password (can be anything), type it and press enter # Pick a MySQL root password (can be anything), type it and press enter
...@@ -293,3 +294,4 @@ Details can be found in the [PostgreSQL][postgres-text-type] and ...@@ -293,3 +294,4 @@ Details can be found in the [PostgreSQL][postgres-text-type] and
[postgres-text-type]: http://www.postgresql.org/docs/9.2/static/datatype-character.html [postgres-text-type]: http://www.postgresql.org/docs/9.2/static/datatype-character.html
[mysql-text-types]: http://dev.mysql.com/doc/refman/5.7/en/string-type-overview.html [mysql-text-types]: http://dev.mysql.com/doc/refman/5.7/en/string-type-overview.html
[ce-38152]: https://gitlab.com/gitlab-org/gitlab-ce/issues/38152
---
last_updated: 2017-09-25
---
# GitLab Kubernetes / OpenShift integration # GitLab Kubernetes / OpenShift integration
GitLab can be configured to interact with Kubernetes, or other systems using the GitLab can be configured to interact with Kubernetes, or other systems using the
...@@ -6,10 +10,6 @@ Kubernetes API (such as OpenShift). ...@@ -6,10 +10,6 @@ Kubernetes API (such as OpenShift).
Each project can be configured to connect to a different Kubernetes cluster, see Each project can be configured to connect to a different Kubernetes cluster, see
the [configuration](#configuration) section. 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.
## Configuration ## Configuration
Navigate to the [Integrations page](project_services.md#accessing-the-project-services) Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
...@@ -47,30 +47,77 @@ The Kubernetes service takes the following parameters: ...@@ -47,30 +47,77 @@ The Kubernetes service takes the following parameters:
[Kubernetes dashboard](https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/#config) [Kubernetes dashboard](https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/#config)
(under **Config > Secrets**). (under **Config > Secrets**).
[namespace]: https://kubernetes.io/docs/user-guide/namespaces/ TIP: **Tip:**
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 [Services Templates](services_templates.md).
## Deployment variables ## Deployment variables
The Kubernetes service exposes following The Kubernetes service exposes the following
[deployment variables](../../../ci/variables/README.md#deployment-variables) in the [deployment variables](../../../ci/variables/README.md#deployment-variables) in the
GitLab CI build environment: GitLab CI/CD build environment:
- `KUBE_URL` - equal to the API URL - `KUBE_URL` - Equal to the API URL.
- `KUBE_TOKEN` - `KUBE_TOKEN` - The Kubernetes token.
- `KUBE_NAMESPACE` - The Kubernetes namespace is auto-generated if not specified. - `KUBE_NAMESPACE` - The Kubernetes namespace is auto-generated if not specified.
The default value is `<project_name>-<project_id>`. You can overwrite it to The default value is `<project_name>-<project_id>`. You can overwrite it to
use different one if needed, otherwise the `KUBE_NAMESPACE` variable will use different one if needed, otherwise the `KUBE_NAMESPACE` variable will
receive the default value. receive the default value.
- `KUBE_CA_PEM_FILE` - only present if a custom CA bundle was specified. Path - `KUBE_CA_PEM_FILE` - Only present if a custom CA bundle was specified. Path
to a file containing PEM data. to a file containing PEM data.
- `KUBE_CA_PEM` (deprecated)- only if a custom CA bundle was specified. Raw PEM data. - `KUBE_CA_PEM` (deprecated) - Only if a custom CA bundle was specified. Raw PEM data.
- `KUBECONFIG` - Path to a file containing kubeconfig for this deployment. CA bundle would be embedded if specified. - `KUBECONFIG` - Path to a file containing `kubeconfig` for this deployment.
CA bundle would be embedded if specified.
## What you can get with the Kubernetes integration
Here's what you can do with GitLab if you enable the Kubernetes integration.
### Deploy Boards (EEP)
> Available in [GitLab Enterprise Edition Premium][ee].
GitLab's Deploy Boards offer a consolidated view of the current health and
status of each CI [environment](../../../ci/environments.md) running on Kubernetes,
displaying the status of the pods in the deployment. Developers and other
teammates can view the progress and status of a rollout, pod by pod, in the
workflow they already use without any need to access Kubernetes.
[> Read more about Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html)
## Web terminals ### Canary Deployments (EEP)
> Available in [GitLab Enterprise Edition Premium][ee].
Leverage [Kubernetes' Canary deployments](https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#canary-deployments)
and visualize your canary deployments right inside the Deploy Board, without
the need to leave GitLab.
[> Read more about Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html)
### Kubernetes monitoring
Automatically detect and monitor Kubernetes metrics. Automatic monitoring of
[NGINX ingress](./prometheus_library/nginx.md) is also supported.
[> Read more about Kubernetes monitoring](prometheus_library/kubernetes.md)
### Auto DevOps
Auto DevOps automatically detects, builds, tests, deploys, and monitors your
applications.
To make full use of Auto DevOps(Auto Deploy, Auto Review Apps, and Auto Monitoring)
you will need the Kubernetes project integration enabled.
[> Read more about Auto DevOps](../../../topics/autodevops/index.md)
### Web terminals
NOTE: **Note:** NOTE: **Note:**
Added in GitLab 8.15. You must be the project owner or have `master` permissions Introduced in GitLab 8.15. You must be the project owner or have `master` permissions
to use terminals. Support is currently limited to the first container in the to use terminals. Support is limited to the first container in the
first pod of your environment. first pod of your environment.
When enabled, the Kubernetes service adds [web terminal](../../../ci/environments.md#web-terminals) When enabled, the Kubernetes service adds [web terminal](../../../ci/environments.md#web-terminals)
...@@ -79,3 +126,5 @@ Docker and Kubernetes, so you get a new shell session within your existing ...@@ -79,3 +126,5 @@ Docker and Kubernetes, so you get a new shell session within your existing
containers. To use this integration, you should deploy to Kubernetes using containers. To use this integration, you should deploy to Kubernetes using
the deployment variables above, ensuring any pods you create are labelled with the deployment variables above, ensuring any pods you create are labelled with
`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest! `app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest!
[ee]: https://about.gitlab.com/gitlab-ee/
# Monitoring AWS Resources # Monitoring AWS Resources
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12621) in GitLab 9.4 > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12621) in GitLab 9.4
GitLab has support for automatically detecting and monitoring AWS resources, starting with the [Elastic Load Balancer](https://aws.amazon.com/elasticloadbalancing/). This is provided by leveraging the official [Cloudwatch exporter](https://github.com/prometheus/cloudwatch_exporter), which translates [Cloudwatch metrics](https://aws.amazon.com/cloudwatch/) into a Prometheus readable form. GitLab has support for automatically detecting and monitoring AWS resources, starting with the [Elastic Load Balancer](https://aws.amazon.com/elasticloadbalancing/). This is provided by leveraging the official [Cloudwatch exporter](https://github.com/prometheus/cloudwatch_exporter), which translates [Cloudwatch metrics](https://aws.amazon.com/cloudwatch/) into a Prometheus readable form.
## Requirements
The [Prometheus service](../prometheus/index.md) must be enabled.
## Metrics supported ## Metrics supported
| Name | Query | | Name | Query |
......
...@@ -3,6 +3,10 @@ ...@@ -3,6 +3,10 @@
GitLab has support for automatically detecting and monitoring HAProxy. This is provided by leveraging the [HAProxy Exporter](https://github.com/prometheus/haproxy_exporter), which translates HAProxy statistics into a Prometheus readable form. GitLab has support for automatically detecting and monitoring HAProxy. This is provided by leveraging the [HAProxy Exporter](https://github.com/prometheus/haproxy_exporter), which translates HAProxy statistics into a Prometheus readable form.
## Requirements
The [Prometheus service](../prometheus/index.md) must be enabled.
## Metrics supported ## Metrics supported
| Name | Query | | Name | Query |
......
# Monitoring Kubernetes # Monitoring Kubernetes
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8935) in GitLab 9.0 > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8935) in GitLab 9.0
GitLab has support for automatically detecting and monitoring Kubernetes metrics. Kubernetes exposes Node level metrics out of the box via the built-in [Prometheus metrics support in cAdvisor](https://github.com/google/cadvisor). No additional services or exporters are needed. GitLab has support for automatically detecting and monitoring Kubernetes metrics.
## Requirements
The [Prometheus](../prometheus.md) and [Kubernetes](../kubernetes.md)
integration services must be enabled.
## Metrics supported ## Metrics supported
...@@ -23,4 +29,4 @@ Prometheus server up and running. You have two options here: ...@@ -23,4 +29,4 @@ Prometheus server up and running. You have two options here:
In order to isolate and only display relevant metrics for a given environment In order to isolate and only display relevant metrics for a given environment
however, GitLab needs a method to detect which labels are associated. To do this, GitLab will [look for an `environment` label](metrics.md#identifying-environments). however, GitLab needs a method to detect which labels are associated. To do this, GitLab will [look for an `environment` label](metrics.md#identifying-environments).
If you are using [GitLab Auto-Deploy][../../../ci/autodeploy/index.md] and one of the two [provided Kubernetes monitoring solutions](../prometheus.md#getting-started-with-prometheus-monitoring), the `environment` label will be automatically added. If you are using [GitLab Auto-Deploy](../../../../ci/autodeploy/index.md) and one of the two [provided Kubernetes monitoring solutions](../prometheus.md#getting-started-with-prometheus-monitoring), the `environment` label will be automatically added.
# Monitoring NGINX # Monitoring NGINX
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12621) in GitLab 9.4 > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12621) in GitLab 9.4
GitLab has support for automatically detecting and monitoring NGINX. This is provided by leveraging the [NGINX VTS exporter](https://github.com/hnlq715/nginx-vts-exporter), which translates [VTS statistics](https://github.com/vozlt/nginx-module-vts) into a Prometheus readable form. GitLab has support for automatically detecting and monitoring NGINX. This is provided by leveraging the [NGINX VTS exporter](https://github.com/hnlq715/nginx-vts-exporter), which translates [VTS statistics](https://github.com/vozlt/nginx-module-vts) into a Prometheus readable form.
## Requirements
The [Prometheus service](../prometheus/index.md) must be enabled.
## Metrics supported ## Metrics supported
| Name | Query | | Name | Query |
......
# Monitoring NGINX Ingress Controller # Monitoring NGINX Ingress Controller
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13438) in GitLab 9.5 > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13438) in GitLab 9.5
GitLab has support for automatically detecting and monitoring the Kubernetes NGINX ingress controller. This is provided by leveraging the built in Prometheus metrics included in [version 0.9.0](https://github.com/kubernetes/ingress/blob/master/controllers/nginx/Changelog.md#09-beta1) of the ingress. GitLab has support for automatically detecting and monitoring the Kubernetes NGINX ingress controller. This is provided by leveraging the built in Prometheus metrics included in [version 0.9.0](https://github.com/kubernetes/ingress/blob/master/controllers/nginx/Changelog.md#09-beta1) of the ingress.
## Requirements
The [Prometheus service](../prometheus/index.md) must be enabled.
## Metrics supported ## Metrics supported
| Name | Query | | Name | Query |
......
...@@ -62,7 +62,7 @@ which is highly recommendable and much faster than hardcoding. ...@@ -62,7 +62,7 @@ which is highly recommendable and much faster than hardcoding.
If you set up a GitLab Pages project on GitLab.com, If you set up a GitLab Pages project on GitLab.com,
it will automatically be accessible under a it will automatically be accessible under a
[subdomain of `namespace.pages.io`](https://docs.gitlab.com/ce/user/project/pages/). [subdomain of `namespace.pages.io`](introduction.md#gitlab-pages-on-gitlab-com).
The `namespace` is defined by your username on GitLab.com, The `namespace` is defined by your username on GitLab.com,
or the group name you created this project under. or the group name you created this project under.
...@@ -73,6 +73,8 @@ Pages wildcard domain. This guide is valid for any GitLab instance, ...@@ -73,6 +73,8 @@ Pages wildcard domain. This guide is valid for any GitLab instance,
you just need to replace Pages wildcard domain on GitLab.com you just need to replace Pages wildcard domain on GitLab.com
(`*.gitlab.io`) with your own. (`*.gitlab.io`) with your own.
Learn more about [namespaces](../../group/index.md#namespaces).
### Practical examples ### Practical examples
#### Project Websites #### Project Websites
......
---
last_updated: 2017-09-28
---
# GitLab Pages from A to Z: Part 3 # GitLab Pages from A to Z: Part 3
> **Article [Type](../../../development/writing_documentation.html#types-of-technical-articles)**: user guide || > **[Article Type](../../../development/writing_documentation.md#types-of-technical-articles)**: user guide ||
> **Level**: beginner || > **Level**: beginner ||
> **Author**: [Marcia Ramos](https://gitlab.com/marcia) || > **Author**: [Marcia Ramos](https://gitlab.com/marcia) ||
> **Publication date:** 2017/02/22 > **Publication date:** 2017-02-22 ||
> **Last updated**: 2017-09-28
- [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md) - [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md)
- [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md) - [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md)
...@@ -16,6 +21,21 @@ As described in the previous part of this series, setting up GitLab Pages with c ...@@ -16,6 +21,21 @@ As described in the previous part of this series, setting up GitLab Pages with c
These steps assume you've already [set your site up](getting_started_part_two.md) and and it's served under the default Pages domain `namespace.gitlab.io`, or `namespace.gitlab.io/project-name`. These steps assume you've already [set your site up](getting_started_part_two.md) and and it's served under the default Pages domain `namespace.gitlab.io`, or `namespace.gitlab.io/project-name`.
### Adding your custom domain to GitLab Pages
To use one or more custom domain with your Pages site, there are two things
you should consider first, which we'll cover in this guide:
1. Either if you're adding a **root domain** or a **subdomain**, for which
you'll need to set up [DNS records](#dns-records)
1. Whether you want to add an [SSL/TLS certificate](#ssl-tls-certificates) or not
To finish the association, you need to [add your domain to your project's Pages settings](#add-your-custom-domain-to-gitlab-pages-settings).
Let's start from the beginning with [DNS records](#dns-records).
If you already know how they work and want to skip the introduction to DNS,
you may be interested in skipping it until the [TL;DR](#tl-dr) section below.
### DNS Records ### DNS Records
A Domain Name System (DNS) web service routes visitors to websites A Domain Name System (DNS) web service routes visitors to websites
...@@ -99,6 +119,29 @@ domain. E.g., **do not** point your `subdomain.domain.com` to ...@@ -99,6 +119,29 @@ domain. E.g., **do not** point your `subdomain.domain.com` to
`namespace.gitlab.io.` or `namespace.gitlab.io/`. `namespace.gitlab.io.` or `namespace.gitlab.io/`.
> - GitLab Pages IP on GitLab.com [has been changed](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/) from `104.208.235.32` to `52.167.214.135`. > - GitLab Pages IP on GitLab.com [has been changed](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/) from `104.208.235.32` to `52.167.214.135`.
### Add your custom domain to GitLab Pages settings
Once you've set the DNS record, you'll need navigate to your project's
**Setting > Pages** and click **+ New domain** to add your custom domain to
GitLab Pages. You can choose whether to add an [SSL/TLS certificate](#ssl-tls-certificates)
to make your website accessible under HTTPS or leave it blank. If don't add a certificate,
your site will be accessible only via HTTP:
![Add new domain](img/add_certificate_to_pages.png)
You can add more than one alias (custom domains and subdomains) to the same project.
An alias can be understood as having many doors leading to the same room.
All the aliases you've set to your site will be listed on **Setting > Pages**.
From that page, you can view, add, and remove them.
Note that [DNS propagation may take some time (up to 24h)](http://www.inmotionhosting.com/support/domain-names/dns-nameserver-changes/domain-names-dns-changes),
although it's usually a matter of minutes to complete. Until it does, visit attempts
to your domain will respond with a 404.
Read through the [general documentation on GitLab Pages](introduction.md#add-a-custom-domain-to-your-pages-website) to learn more about adding
custom domains to GitLab Pages sites.
### SSL/TLS Certificates ### SSL/TLS Certificates
Every GitLab Pages project on GitLab.com will be available under Every GitLab Pages project on GitLab.com will be available under
......
...@@ -28,7 +28,8 @@ In general there are two types of pages one might create: ...@@ -28,7 +28,8 @@ In general there are two types of pages one might create:
- Pages per project (`username.example.io/projectname` or `groupname.example.io/projectname`) - Pages per project (`username.example.io/projectname` or `groupname.example.io/projectname`)
In GitLab, usernames and groupnames are unique and we often refer to them In GitLab, usernames and groupnames are unique and we often refer to them
as namespaces. There can be only one namespace in a GitLab instance. Below you as [namespaces](../../group/index.md#namespaces). There can be only one namespace
in a GitLab instance. Below you
can see the connection between the type of GitLab Pages, what the project name can see the connection between the type of GitLab Pages, what the project name
that is created on GitLab looks like and the website URL it will be ultimately that is created on GitLab looks like and the website URL it will be ultimately
be served on. be served on.
...@@ -98,6 +99,9 @@ The steps to create a project page for a user or a group are identical: ...@@ -98,6 +99,9 @@ The steps to create a project page for a user or a group are identical:
A user's project will be served under `http(s)://username.example.io/projectname` A user's project will be served under `http(s)://username.example.io/projectname`
whereas a group's project under `http(s)://groupname.example.io/projectname`. whereas a group's project under `http(s)://groupname.example.io/projectname`.
For practical examples for group and project Pages, read through the guide
[GitLab Pages from A to Z: Part 1 - Static sites and GitLab Pages domains](getting_started_part_one.md#practical-examples).
## Quick Start ## Quick Start
Read through [GitLab Pages Quick Start Guide][pages-quick] or watch the video tutorial on Read through [GitLab Pages Quick Start Guide][pages-quick] or watch the video tutorial on
...@@ -111,6 +115,9 @@ The key thing about GitLab Pages is the `.gitlab-ci.yml` file, something that ...@@ -111,6 +115,9 @@ The key thing about GitLab Pages is the `.gitlab-ci.yml` file, something that
gives you absolute control over the build process. You can actually watch your gives you absolute control over the build process. You can actually watch your
website being built live by following the CI job traces. website being built live by following the CI job traces.
For a simplified user guide on setting up GitLab CI/CD for Pages, read through
the article [GitLab Pages from A to Z: Part 4 - Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md#creating-and-tweaking-gitlab-ci-yml-for-gitlab-pages)
> **Note:** > **Note:**
> Before reading this section, make sure you familiarize yourself with GitLab CI > Before reading this section, make sure you familiarize yourself with GitLab CI
> and the specific syntax of[`.gitlab-ci.yml`][yaml] by > and the specific syntax of[`.gitlab-ci.yml`][yaml] by
...@@ -311,6 +318,9 @@ Visit the GitLab Pages group for a full list of example projects: ...@@ -311,6 +318,9 @@ Visit the GitLab Pages group for a full list of example projects:
### Add a custom domain to your Pages website ### Add a custom domain to your Pages website
For a complete guide on Pages domains, read through the article
[GitLab Pages from A to Z: Part 3 - Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md#setting-up-custom-domains-dns-records-and-ssl-tls-certificates)
If this setting is enabled by your GitLab administrator, you should be able to If this setting is enabled by your GitLab administrator, you should be able to
see the **New Domain** button when visiting your project's settings through the see the **New Domain** button when visiting your project's settings through the
gear icon in the top right and then navigating to **Pages**. gear icon in the top right and then navigating to **Pages**.
...@@ -349,6 +359,9 @@ private key when adding a new domain. ...@@ -349,6 +359,9 @@ private key when adding a new domain.
![Pages upload cert](img/pages_upload_cert.png) ![Pages upload cert](img/pages_upload_cert.png)
For a complete guide on Pages domains, read through the article
[GitLab Pages from A to Z: Part 3 - Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md#setting-up-custom-domains-dns-records-and-ssl-tls-certificates)
### Custom error codes pages ### Custom error codes pages
You can provide your own 403 and 404 error pages by creating the `403.html` and You can provide your own 403 and 404 error pages by creating the `403.html` and
...@@ -387,6 +400,8 @@ If you are using GitLab.com to host your website, then: ...@@ -387,6 +400,8 @@ If you are using GitLab.com to host your website, then:
The rest of the guide still applies. The rest of the guide still applies.
See also: [GitLab Pages from A to Z: Part 1 - Static sites and GitLab Pages domains](getting_started_part_one.md#gitlab-pages-domain).
## Limitations ## Limitations
When using Pages under the general domain of a GitLab instance (`*.example.io`), When using Pages under the general domain of a GitLab instance (`*.example.io`),
......
...@@ -77,7 +77,7 @@ module API ...@@ -77,7 +77,7 @@ module API
raise RevokedError raise RevokedError
when AccessTokenValidationService::VALID when AccessTokenValidationService::VALID
@current_user = User.find(access_token.resource_owner_id) User.find(access_token.resource_owner_id)
end end
end end
...@@ -86,7 +86,13 @@ module API ...@@ -86,7 +86,13 @@ module API
return nil unless token_string.present? return nil unless token_string.present?
find_user_by_authentication_token(token_string) || find_user_by_personal_access_token(token_string, scopes) user =
find_user_by_authentication_token(token_string) ||
find_user_by_personal_access_token(token_string, scopes)
raise UnauthorizedError unless user
user
end end
def find_user_by_job_token def find_user_by_job_token
...@@ -99,10 +105,6 @@ module API ...@@ -99,10 +105,6 @@ module API
end end
end end
def current_user
@current_user
end
private private
def route_authentication_setting def route_authentication_setting
...@@ -125,7 +127,16 @@ module API ...@@ -125,7 +127,16 @@ module API
end end
def find_access_token def find_access_token
@access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods) return @access_token if defined?(@access_token)
token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods)
return @access_token = nil unless token
@access_token = Doorkeeper::AccessToken.by_token(token)
raise UnauthorizedError unless @access_token
@access_token.revoke_previous_refresh_token!
@access_token
end end
def doorkeeper_request def doorkeeper_request
...@@ -187,6 +198,7 @@ module API ...@@ -187,6 +198,7 @@ module API
TokenNotFoundError = Class.new(StandardError) TokenNotFoundError = Class.new(StandardError)
ExpiredError = Class.new(StandardError) ExpiredError = Class.new(StandardError)
RevokedError = Class.new(StandardError) RevokedError = Class.new(StandardError)
UnauthorizedError = Class.new(StandardError)
class InsufficientScopeError < StandardError class InsufficientScopeError < StandardError
attr_reader :scopes attr_reader :scopes
......
module API
module CustomAttributesEndpoints
extend ActiveSupport::Concern
included do
attributable_class = name.demodulize.singularize
attributable_key = attributable_class.underscore
attributable_name = attributable_class.humanize(capitalize: false)
attributable_finder = "find_#{attributable_key}"
helpers do
params :custom_attributes_key do
requires :key, type: String, desc: 'The key of the custom attribute'
end
end
desc "Get all custom attributes on a #{attributable_name}" do
success Entities::CustomAttribute
end
get ':id/custom_attributes' do
resource = public_send(attributable_finder, params[:id]) # rubocop:disable GitlabSecurity/PublicSend
authorize! :read_custom_attribute
present resource.custom_attributes, with: Entities::CustomAttribute
end
desc "Get a custom attribute on a #{attributable_name}" do
success Entities::CustomAttribute
end
params do
use :custom_attributes_key
end
get ':id/custom_attributes/:key' do
resource = public_send(attributable_finder, params[:id]) # rubocop:disable GitlabSecurity/PublicSend
authorize! :read_custom_attribute
custom_attribute = resource.custom_attributes.find_by!(key: params[:key])
present custom_attribute, with: Entities::CustomAttribute
end
desc "Set a custom attribute on a #{attributable_name}"
params do
use :custom_attributes_key
requires :value, type: String, desc: 'The value of the custom attribute'
end
put ':id/custom_attributes/:key' do
resource = public_send(attributable_finder, params[:id]) # rubocop:disable GitlabSecurity/PublicSend
authorize! :update_custom_attribute
custom_attribute = resource.custom_attributes
.find_or_initialize_by(key: params[:key])
custom_attribute.update(value: params[:value])
if custom_attribute.valid?
present custom_attribute, with: Entities::CustomAttribute
else
render_validation_error!(custom_attribute)
end
end
desc "Delete a custom attribute on a #{attributable_name}"
params do
use :custom_attributes_key
end
delete ':id/custom_attributes/:key' do
resource = public_send(attributable_finder, params[:id]) # rubocop:disable GitlabSecurity/PublicSend
authorize! :update_custom_attribute
resource.custom_attributes.find_by!(key: params[:key]).destroy
status 204
end
end
end
end
...@@ -1156,5 +1156,10 @@ module API ...@@ -1156,5 +1156,10 @@ module API
expose :failing_on_hosts expose :failing_on_hosts
expose :total_failures expose :total_failures
end end
class CustomAttribute < Grape::Entity
expose :key
expose :value
end
end end
end end
...@@ -5,6 +5,8 @@ module API ...@@ -5,6 +5,8 @@ module API
include Gitlab::Utils include Gitlab::Utils
include Helpers::Pagination include Helpers::Pagination
UnauthorizedError = Class.new(StandardError)
SUDO_HEADER = "HTTP_SUDO".freeze SUDO_HEADER = "HTTP_SUDO".freeze
SUDO_PARAM = :sudo SUDO_PARAM = :sudo
...@@ -159,7 +161,7 @@ module API ...@@ -159,7 +161,7 @@ module API
end end
def authenticate! def authenticate!
unauthorized! unless current_user && can?(initial_current_user, :access_api) unauthorized! unless current_user
end end
def authenticate_non_get! def authenticate_non_get!
...@@ -422,18 +424,25 @@ module API ...@@ -422,18 +424,25 @@ module API
def initial_current_user def initial_current_user
return @initial_current_user if defined?(@initial_current_user) return @initial_current_user if defined?(@initial_current_user)
Gitlab::Auth::UniqueIpsLimiter.limit_user! do begin
@initial_current_user ||= find_user_by_private_token(scopes: scopes_registered_for_endpoint) @initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user }
@initial_current_user ||= doorkeeper_guard(scopes: scopes_registered_for_endpoint) rescue APIGuard::UnauthorizedError, UnauthorizedError
@initial_current_user ||= find_user_from_warden unauthorized!
@initial_current_user ||= find_user_by_job_token end
end
def find_current_user
user =
find_user_by_private_token(scopes: scopes_registered_for_endpoint) ||
doorkeeper_guard(scopes: scopes_registered_for_endpoint) ||
find_user_from_warden ||
find_user_by_job_token
unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed? return nil unless user
@initial_current_user = nil
end
@initial_current_user raise UnauthorizedError unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
end
user
end end
def sudo! def sudo!
......
...@@ -6,6 +6,8 @@ module API ...@@ -6,6 +6,8 @@ module API
allow_access_with_scope :read_user, if: -> (request) { request.get? } allow_access_with_scope :read_user, if: -> (request) { request.get? }
resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
include CustomAttributesEndpoints
before do before do
authenticate_non_get! authenticate_non_get!
end end
...@@ -332,7 +334,7 @@ module API ...@@ -332,7 +334,7 @@ module API
user = User.find_by(id: params.delete(:id)) user = User.find_by(id: params.delete(:id))
not_found!('User') unless user not_found!('User') unless user
email = Emails::CreateService.new(current_user, declared_params(include_missing: false).merge(user: user)).execute email = Emails::CreateService.new(current_user, declared_params(include_missing: false).merge(user: user)).execute
if email.errors.blank? if email.errors.blank?
NotificationService.new.new_email(email) NotificationService.new.new_email(email)
......
require_relative 'error' require_relative 'error'
require_relative 'import/issue'
require_relative 'import/legacy_diff_note'
require_relative 'import/merge_request'
require_relative 'import/note'
module Github module Github
class Import class Import
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
class MergeRequest < ::MergeRequest
self.table_name = 'merge_requests'
self.reset_callbacks :create
self.reset_callbacks :save
self.reset_callbacks :commit
self.reset_callbacks :update
self.reset_callbacks :validate
end
class Issue < ::Issue
self.table_name = 'issues'
self.reset_callbacks :save
self.reset_callbacks :create
self.reset_callbacks :commit
self.reset_callbacks :update
self.reset_callbacks :validate
end
class Note < ::Note
self.table_name = 'notes'
self.reset_callbacks :save
self.reset_callbacks :commit
self.reset_callbacks :update
self.reset_callbacks :validate
end
class LegacyDiffNote < ::LegacyDiffNote
self.table_name = 'notes'
self.reset_callbacks :commit
self.reset_callbacks :update
self.reset_callbacks :validate
end
attr_reader :project, :repository, :repo, :repo_url, :wiki_url, attr_reader :project, :repository, :repo, :repo_url, :wiki_url,
:options, :errors, :cached, :verbose :options, :errors, :cached, :verbose
......
module Github
class Import
class Issue < ::Issue
self.table_name = 'issues'
self.reset_callbacks :save
self.reset_callbacks :create
self.reset_callbacks :commit
self.reset_callbacks :update
self.reset_callbacks :validate
end
end
end
module Github
class Import
class LegacyDiffNote < ::LegacyDiffNote
self.table_name = 'notes'
self.store_full_sti_class = false
self.reset_callbacks :commit
self.reset_callbacks :update
self.reset_callbacks :validate
end
end
end
module Github
class Import
class MergeRequest < ::MergeRequest
self.table_name = 'merge_requests'
self.reset_callbacks :create
self.reset_callbacks :save
self.reset_callbacks :commit
self.reset_callbacks :update
self.reset_callbacks :validate
end
end
end
module Github
class Import
class Note < ::Note
self.table_name = 'notes'
self.store_full_sti_class = false
self.reset_callbacks :save
self.reset_callbacks :commit
self.reset_callbacks :update
self.reset_callbacks :validate
end
end
end
...@@ -20,6 +20,7 @@ module Gitlab ...@@ -20,6 +20,7 @@ module Gitlab
GitError = Class.new(StandardError) GitError = Class.new(StandardError)
DeleteBranchError = Class.new(StandardError) DeleteBranchError = Class.new(StandardError)
CreateTreeError = Class.new(StandardError) CreateTreeError = Class.new(StandardError)
TagExistsError = Class.new(StandardError)
class << self class << self
# Unlike `new`, `create` takes the repository path # Unlike `new`, `create` takes the repository path
...@@ -70,8 +71,6 @@ module Gitlab ...@@ -70,8 +71,6 @@ module Gitlab
delegate :empty?, delegate :empty?,
to: :rugged to: :rugged
delegate :exists?, to: :gitaly_repository_client
def ==(other) def ==(other)
path == other.path path == other.path
end end
...@@ -99,6 +98,18 @@ module Gitlab ...@@ -99,6 +98,18 @@ module Gitlab
@circuit_breaker ||= Gitlab::Git::Storage::CircuitBreaker.for_storage(storage) @circuit_breaker ||= Gitlab::Git::Storage::CircuitBreaker.for_storage(storage)
end end
def exists?
Gitlab::GitalyClient.migrate(:repository_exists) do |enabled|
if enabled
gitaly_repository_client.exists?
else
circuit_breaker.perform do
File.exist?(File.join(@path, 'refs'))
end
end
end
end
# Returns an Array of branch names # Returns an Array of branch names
# sorted by name ASC # sorted by name ASC
def branch_names def branch_names
...@@ -633,24 +644,13 @@ module Gitlab ...@@ -633,24 +644,13 @@ module Gitlab
end end
def add_tag(tag_name, user:, target:, message: nil) def add_tag(tag_name, user:, target:, message: nil)
target_object = Ref.dereference_object(lookup(target)) gitaly_migrate(:operation_user_add_tag) do |is_enabled|
raise InvalidRef.new("target not found: #{target}") unless target_object if is_enabled
gitaly_add_tag(tag_name, user: user, target: target, message: message)
user = Gitlab::Git::User.from_gitlab(user) unless user.respond_to?(:gl_id) else
rugged_add_tag(tag_name, user: user, target: target, message: message)
options = nil # Use nil, not the empty hash. Rugged cares about this. end
if message
options = {
message: message,
tagger: Gitlab::Git.committer_hash(email: user.email, name: user.name)
}
end end
OperationService.new(user, self).add_tag(tag_name, target_object.oid, options)
find_tag(tag_name)
rescue Rugged::ReferenceError => ex
raise InvalidRef, ex
end end
def rm_branch(branch_name, user:) def rm_branch(branch_name, user:)
...@@ -658,7 +658,13 @@ module Gitlab ...@@ -658,7 +658,13 @@ module Gitlab
end end
def rm_tag(tag_name, user:) def rm_tag(tag_name, user:)
OperationService.new(user, self).rm_tag(find_tag(tag_name)) gitaly_migrate(:operation_user_delete_tag) do |is_enabled|
if is_enabled
gitaly_operations_client.rm_tag(tag_name, user)
else
Gitlab::Git::OperationService.new(user, self).rm_tag(find_tag(tag_name))
end
end
end end
def find_tag(name) def find_tag(name)
...@@ -934,7 +940,11 @@ module Gitlab ...@@ -934,7 +940,11 @@ module Gitlab
if start_repository == self if start_repository == self
yield commit(start_branch_name) yield commit(start_branch_name)
else else
sha = start_repository.commit(start_branch_name).sha start_commit = start_repository.commit(start_branch_name)
return yield nil unless start_commit
sha = start_commit.sha
if branch_commit = commit(sha) if branch_commit = commit(sha)
yield branch_commit yield branch_commit
...@@ -963,8 +973,9 @@ module Gitlab ...@@ -963,8 +973,9 @@ module Gitlab
with_repo_branch_commit(source_repository, source_branch) do |commit| with_repo_branch_commit(source_repository, source_branch) do |commit|
if commit if commit
write_ref(local_ref, commit.sha) write_ref(local_ref, commit.sha)
true
else else
raise Rugged::ReferenceError, 'source repository is empty' false
end end
end end
end end
...@@ -1030,6 +1041,10 @@ module Gitlab ...@@ -1030,6 +1041,10 @@ module Gitlab
Gitlab::GitalyClient::Util.repository(@storage, @relative_path) Gitlab::GitalyClient::Util.repository(@storage, @relative_path)
end end
def gitaly_operations_client
@gitaly_operations_client ||= Gitlab::GitalyClient::OperationService.new(self)
end
def gitaly_ref_client def gitaly_ref_client
@gitaly_ref_client ||= Gitlab::GitalyClient::RefService.new(self) @gitaly_ref_client ||= Gitlab::GitalyClient::RefService.new(self)
end end
...@@ -1364,6 +1379,33 @@ module Gitlab ...@@ -1364,6 +1379,33 @@ module Gitlab
false false
end end
def gitaly_add_tag(tag_name, user:, target:, message: nil)
gitaly_operations_client.add_tag(tag_name, user, target, message)
end
def rugged_add_tag(tag_name, user:, target:, message: nil)
target_object = Ref.dereference_object(lookup(target))
raise InvalidRef.new("target not found: #{target}") unless target_object
user = Gitlab::Git::User.from_gitlab(user) unless user.respond_to?(:gl_id)
options = nil # Use nil, not the empty hash. Rugged cares about this.
if message
options = {
message: message,
tagger: Gitlab::Git.committer_hash(email: user.email, name: user.name)
}
end
Gitlab::Git::OperationService.new(user, self).add_tag(tag_name, target_object.oid, options)
find_tag(tag_name)
rescue Rugged::ReferenceError => ex
raise InvalidRef, ex
rescue Rugged::TagError
raise TagExistsError
end
def rugged_create_branch(ref, start_point) def rugged_create_branch(ref, start_point)
rugged_ref = rugged.branches.create(ref, start_point) rugged_ref = rugged.branches.create(ref, start_point)
target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target) target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
......
...@@ -19,6 +19,7 @@ module Gitlab ...@@ -19,6 +19,7 @@ module Gitlab
command_not_allowed: "The command you're trying to execute is not allowed.", command_not_allowed: "The command you're trying to execute is not allowed.",
upload_pack_disabled_over_http: 'Pulling over HTTP is not allowed.', upload_pack_disabled_over_http: 'Pulling over HTTP is not allowed.',
receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.', receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.',
readonly: 'The repository is temporarily read-only. Please try again later.',
cannot_push_to_secondary_geo: "You can't push code to a secondary GitLab Geo node." cannot_push_to_secondary_geo: "You can't push code to a secondary GitLab Geo node."
}.freeze }.freeze
...@@ -172,7 +173,7 @@ module Gitlab ...@@ -172,7 +173,7 @@ module Gitlab
# TODO: please clean this up # TODO: please clean this up
def check_push_access!(changes) def check_push_access!(changes)
if project.repository_read_only? if project.repository_read_only?
raise UnauthorizedError, 'The repository is temporarily read-only. Please try again later.' raise UnauthorizedError, ERROR_MESSAGES[:readonly]
end end
if Gitlab::Geo.secondary? if Gitlab::Geo.secondary?
......
module Gitlab
module GitalyClient
class OperationService
def initialize(repository)
@gitaly_repo = repository.gitaly_repository
@repository = repository
end
def rm_tag(tag_name, user)
request = Gitaly::UserDeleteTagRequest.new(
repository: @gitaly_repo,
tag_name: GitalyClient.encode(tag_name),
user: Util.gitaly_user(user)
)
response = GitalyClient.call(@repository.storage, :operation_service, :user_delete_tag, request)
if pre_receive_error = response.pre_receive_error.presence
raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error
end
end
def add_tag(tag_name, user, target, message)
request = Gitaly::UserCreateTagRequest.new(
repository: @gitaly_repo,
user: Util.gitaly_user(user),
tag_name: GitalyClient.encode(tag_name),
target_revision: GitalyClient.encode(target),
message: GitalyClient.encode(message.to_s)
)
response = GitalyClient.call(@repository.storage, :operation_service, :user_create_tag, request)
if pre_receive_error = response.pre_receive_error.presence
raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error
elsif response.exists
raise Gitlab::Git::Repository::TagExistsError
end
Util.gitlab_tag_from_gitaly_tag(@repository, response.tag)
rescue GRPC::FailedPrecondition => e
raise Gitlab::Git::Repository::InvalidRef, e
end
end
end
end
...@@ -155,19 +155,7 @@ module Gitlab ...@@ -155,19 +155,7 @@ module Gitlab
def consume_tags_response(response) def consume_tags_response(response)
response.flat_map do |message| response.flat_map do |message|
message.tags.map do |gitaly_tag| message.tags.map { |gitaly_tag| Util.gitlab_tag_from_gitaly_tag(@repository, gitaly_tag) }
if gitaly_tag.target_commit.present?
gitaly_commit = Gitlab::Git::Commit.decorate(@repository, gitaly_tag.target_commit)
end
Gitlab::Git::Tag.new(
@repository,
encode!(gitaly_tag.name.dup),
gitaly_tag.id,
gitaly_commit,
encode!(gitaly_tag.message.chomp)
)
end
end end
end end
......
...@@ -10,6 +10,30 @@ module Gitlab ...@@ -10,6 +10,30 @@ module Gitlab
git_alternate_object_directories: Array.wrap(Gitlab::Git::Env['GIT_ALTERNATE_OBJECT_DIRECTORIES']) git_alternate_object_directories: Array.wrap(Gitlab::Git::Env['GIT_ALTERNATE_OBJECT_DIRECTORIES'])
) )
end end
def gitaly_user(gitlab_user)
return unless gitlab_user
Gitaly::User.new(
gl_id: Gitlab::GlId.gl_id(gitlab_user),
name: GitalyClient.encode(gitlab_user.name),
email: GitalyClient.encode(gitlab_user.email)
)
end
def gitlab_tag_from_gitaly_tag(repository, gitaly_tag)
if gitaly_tag.target_commit.present?
commit = Gitlab::Git::Commit.decorate(repository, gitaly_tag.target_commit)
end
Gitlab::Git::Tag.new(
repository,
Gitlab::EncodingHelper.encode!(gitaly_tag.name.dup),
gitaly_tag.id,
commit,
Gitlab::EncodingHelper.encode!(gitaly_tag.message.chomp)
)
end
end end
end end
end end
......
...@@ -1523,7 +1523,7 @@ msgstr "" ...@@ -1523,7 +1523,7 @@ msgstr ""
msgid "There are problems accessing Git storage: " msgid "There are problems accessing Git storage: "
msgstr "" msgstr ""
msgid "This is the author's first Merge Request to this project. Handle with care." msgid "This is the author's first Merge Request to this project."
msgstr "" msgstr ""
msgid "This means you can not push code until you create an empty repository or import existing one." msgid "This means you can not push code until you create an empty repository or import existing one."
......
FactoryGirl.define do
factory :user_custom_attribute do
user
sequence(:key) { |n| "key#{n}" }
sequence(:value) { |n| "value#{n}" }
end
end
...@@ -104,6 +104,6 @@ describe 'Recent searches', js: true do ...@@ -104,6 +104,6 @@ describe 'Recent searches', js: true do
set_recent_searches(project_1_local_storage_key, 'fail') set_recent_searches(project_1_local_storage_key, 'fail')
visit project_issues_path(project_1) visit project_issues_path(project_1)
expect(find('.flash-alert')).to have_text('An error occured while parsing recent searches') expect(find('.flash-alert')).to have_text('An error occurred while parsing recent searches')
end end
end end
...@@ -131,6 +131,14 @@ describe 'Issues' do ...@@ -131,6 +131,14 @@ describe 'Issues' do
end end
describe 'Issue info' do describe 'Issue info' do
it 'links to current issue in breadcrubs' do
issue = create(:issue, project: project)
visit project_issue_path(project, issue)
expect(find('.breadcrumbs-sub-title a')[:href]).to end_with(issue_path(issue))
end
it 'excludes award_emoji from comment count' do it 'excludes award_emoji from comment count' do
issue = create(:issue, author: user, assignees: [user], project: project, title: 'foobar') issue = create(:issue, author: user, assignees: [user], project: project, title: 'foobar')
create(:award_emoji, awardable: issue) create(:award_emoji, awardable: issue)
......
require 'spec_helper'
feature 'Commit diff', :js do
include RepoHelpers
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) }
before do
project.add_master(user)
sign_in user
end
%w(inline parallel).each do |view|
context "#{view} view" do
before do
visit project_commit_path(project, sample_commit.id, view: view)
end
it "adds comment to diff" do
diff_line_num = first('.diff-line-num.new')
diff_line_num.trigger('mouseover')
diff_line_num.find('.js-add-diff-note-button').trigger('click')
page.within(first('.diff-viewer')) do
find('.js-note-text').set 'test comment'
click_button 'Comment'
expect(page).to have_content('test comment')
end
end
end
end
end
...@@ -40,7 +40,7 @@ describe 'User updates wiki page' do ...@@ -40,7 +40,7 @@ describe 'User updates wiki page' do
expect(current_path).to include('one/two/three-test') expect(current_path).to include('one/two/three-test')
expect(find('.wiki-pages')).to have_content('Three') expect(find('.wiki-pages')).to have_content('Three')
click_on('Three') first(:link, text: 'Three').click
expect(find('.nav-text')).to have_content('Three') expect(find('.nav-text')).to have_content('Three')
......
...@@ -34,7 +34,7 @@ describe 'User views a wiki page' do ...@@ -34,7 +34,7 @@ describe 'User views a wiki page' do
it 'shows the history of a page that has a path', :js do it 'shows the history of a page that has a path', :js do
expect(current_path).to include('one/two/three-test') expect(current_path).to include('one/two/three-test')
click_on('Three') first(:link, text: 'Three').click
click_on('Page history') click_on('Page history')
expect(current_path).to include('one/two/three-test') expect(current_path).to include('one/two/three-test')
...@@ -48,7 +48,7 @@ describe 'User views a wiki page' do ...@@ -48,7 +48,7 @@ describe 'User views a wiki page' do
expect(current_path).to include('one/two/three-test') expect(current_path).to include('one/two/three-test')
expect(find('.wiki-pages')).to have_content('Three') expect(find('.wiki-pages')).to have_content('Three')
click_on('Three') first(:link, text: 'Three').click
expect(find('.nav-text')).to have_content('Three') expect(find('.nav-text')).to have_content('Three')
......
...@@ -35,15 +35,30 @@ feature 'Master deletes tag' do ...@@ -35,15 +35,30 @@ feature 'Master deletes tag' do
end end
context 'when pre-receive hook fails', js: true do context 'when pre-receive hook fails', js: true do
before do context 'when Gitaly operation_user_delete_tag feature is enabled' do
allow_any_instance_of(Gitlab::Git::HooksService).to receive(:execute) before do
.and_raise(Gitlab::Git::HooksService::PreReceiveError, 'Do not delete tags') allow_any_instance_of(Gitlab::GitalyClient::OperationService).to receive(:rm_tag)
.and_raise(Gitlab::Git::HooksService::PreReceiveError, 'Do not delete tags')
end
scenario 'shows the error message' do
delete_first_tag
expect(page).to have_content('Do not delete tags')
end
end end
scenario 'shows the error message' do context 'when Gitaly operation_user_delete_tag feature is disabled', skip_gitaly_mock: true do
delete_first_tag before do
allow_any_instance_of(Gitlab::Git::HooksService).to receive(:execute)
.and_raise(Gitlab::Git::HooksService::PreReceiveError, 'Do not delete tags')
end
scenario 'shows the error message' do
delete_first_tag
expect(page).to have_content('Do not delete tags') expect(page).to have_content('Do not delete tags')
end
end end
end end
......
...@@ -72,6 +72,15 @@ describe UsersFinder do ...@@ -72,6 +72,15 @@ describe UsersFinder do
expect(users.map(&:username)).not_to include([filtered_user_before.username, filtered_user_after.username]) expect(users.map(&:username)).not_to include([filtered_user_before.username, filtered_user_after.username])
end end
it 'does not filter by custom attributes' do
users = described_class.new(
user,
custom_attributes: { foo: 'bar' }
).execute
expect(users).to contain_exactly(user, user1, user2, omniauth_user)
end
end end
context 'with an admin user' do context 'with an admin user' do
...@@ -88,6 +97,19 @@ describe UsersFinder do ...@@ -88,6 +97,19 @@ describe UsersFinder do
expect(users).to contain_exactly(admin, user1, user2, external_user, omniauth_user) expect(users).to contain_exactly(admin, user1, user2, external_user, omniauth_user)
end end
it 'filters by custom attributes' do
create :user_custom_attribute, user: user1, key: 'foo', value: 'foo'
create :user_custom_attribute, user: user1, key: 'bar', value: 'bar'
create :user_custom_attribute, user: user2, key: 'foo', value: 'foo'
users = described_class.new(
admin,
custom_attributes: { foo: 'foo', bar: 'bar' }
).execute
expect(users).to contain_exactly(user1)
end
end end
end end
end end
...@@ -117,7 +117,7 @@ describe('iPython notebook renderer', () => { ...@@ -117,7 +117,7 @@ describe('iPython notebook renderer', () => {
it('shows error message', () => { it('shows error message', () => {
expect( expect(
document.querySelector('.md').textContent.trim(), document.querySelector('.md').textContent.trim(),
).toBe('An error occured whilst parsing the file.'); ).toBe('An error occurred whilst parsing the file.');
}); });
}); });
...@@ -153,7 +153,7 @@ describe('iPython notebook renderer', () => { ...@@ -153,7 +153,7 @@ describe('iPython notebook renderer', () => {
it('shows error message', () => { it('shows error message', () => {
expect( expect(
document.querySelector('.md').textContent.trim(), document.querySelector('.md').textContent.trim(),
).toBe('An error occured whilst loading the file. Please try again later.'); ).toBe('An error occurred whilst loading the file. Please try again later.');
}); });
}); });
}); });
...@@ -76,7 +76,7 @@ describe('PDF renderer', () => { ...@@ -76,7 +76,7 @@ describe('PDF renderer', () => {
it('shows error message', () => { it('shows error message', () => {
expect( expect(
document.querySelector('.md').textContent.trim(), document.querySelector('.md').textContent.trim(),
).toBe('An error occured whilst loading the file. Please try again later.'); ).toBe('An error occurred whilst loading the file. Please try again later.');
}); });
}); });
}); });
...@@ -289,4 +289,18 @@ describe('Build', () => { ...@@ -289,4 +289,18 @@ describe('Build', () => {
}); });
}); });
}); });
describe('getBuildTrace', () => {
it('should request build trace with state parameter', (done) => {
spyOn(jQuery, 'ajax').and.callThrough();
new Build();
setTimeout(() => {
expect(jQuery.ajax).toHaveBeenCalledWith(
{ url: `${BUILD_URL}/trace.json`, data: { state: '' } },
);
done();
}, 0);
});
});
}); });
...@@ -21,13 +21,11 @@ describe('RepoEditButton', () => { ...@@ -21,13 +21,11 @@ describe('RepoEditButton', () => {
expect(vm.$el.textContent).toMatch('Edit'); expect(vm.$el.textContent).toMatch('Edit');
spyOn(vm, 'editCancelClicked').and.callThrough(); spyOn(vm, 'editCancelClicked').and.callThrough();
spyOn(vm, 'toggleProjectRefsForm');
vm.$el.click(); vm.$el.click();
Vue.nextTick(() => { Vue.nextTick(() => {
expect(vm.editCancelClicked).toHaveBeenCalled(); expect(vm.editCancelClicked).toHaveBeenCalled();
expect(vm.toggleProjectRefsForm).toHaveBeenCalled();
expect(vm.$el.textContent).toMatch('Cancel edit'); expect(vm.$el.textContent).toMatch('Cancel edit');
done(); done();
}); });
......
...@@ -79,6 +79,20 @@ describe('RepoSidebar', () => { ...@@ -79,6 +79,20 @@ describe('RepoSidebar', () => {
expect(Helper.getContent).toHaveBeenCalledWith(file1); expect(Helper.getContent).toHaveBeenCalledWith(file1);
}); });
it('should not fetch data for already opened files', () => {
const file = {
id: 42,
url: 'foo',
};
spyOn(Helper, 'getFileFromPath').and.returnValue(file);
spyOn(RepoStore, 'setActiveFiles');
const vm = createComponent();
vm.fileClicked(file);
expect(RepoStore.setActiveFiles).toHaveBeenCalledWith(file);
});
it('should hide files in directory if already open', () => { it('should hide files in directory if already open', () => {
spyOn(RepoStore, 'removeChildFilesOfTree').and.callThrough(); spyOn(RepoStore, 'removeChildFilesOfTree').and.callThrough();
const file1 = { const file1 = {
......
require 'spec_helper'
describe Github::Import::LegacyDiffNote do
describe '#type' do
it 'returns the original note type' do
expect(described_class.new.type).to eq('LegacyDiffNote')
end
end
end
require 'spec_helper'
describe Github::Import::Note do
describe '#type' do
it 'returns the original note type' do
expect(described_class.new.type).to eq('Note')
end
end
end
...@@ -1332,6 +1332,84 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -1332,6 +1332,84 @@ describe Gitlab::Git::Repository, seed_helper: true do
end end
end end
describe '#with_repo_branch_commit' do
context 'when comparing with the same repository' do
let(:start_repository) { repository }
context 'when the branch exists' do
let(:start_branch_name) { 'master' }
it 'yields the commit' do
expect { |b| repository.with_repo_branch_commit(start_repository, start_branch_name, &b) }
.to yield_with_args(an_instance_of(Gitlab::Git::Commit))
end
end
context 'when the branch does not exist' do
let(:start_branch_name) { 'definitely-not-master' }
it 'yields nil' do
expect { |b| repository.with_repo_branch_commit(start_repository, start_branch_name, &b) }
.to yield_with_args(nil)
end
end
end
context 'when comparing with another repository' do
let(:start_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
context 'when the branch exists' do
let(:start_branch_name) { 'master' }
it 'yields the commit' do
expect { |b| repository.with_repo_branch_commit(start_repository, start_branch_name, &b) }
.to yield_with_args(an_instance_of(Gitlab::Git::Commit))
end
end
context 'when the branch does not exist' do
let(:start_branch_name) { 'definitely-not-master' }
it 'yields nil' do
expect { |b| repository.with_repo_branch_commit(start_repository, start_branch_name, &b) }
.to yield_with_args(nil)
end
end
end
end
describe '#fetch_source_branch' do
let(:local_ref) { 'refs/merge-requests/1/head' }
context 'when the branch exists' do
let(:source_branch) { 'master' }
it 'writes the ref' do
expect(repository).to receive(:write_ref).with(local_ref, /\h{40}/)
repository.fetch_source_branch(repository, source_branch, local_ref)
end
it 'returns true' do
expect(repository.fetch_source_branch(repository, source_branch, local_ref)).to eq(true)
end
end
context 'when the branch does not exist' do
let(:source_branch) { 'definitely-not-master' }
it 'does not write the ref' do
expect(repository).not_to receive(:write_ref)
repository.fetch_source_branch(repository, source_branch, local_ref)
end
it 'returns false' do
expect(repository.fetch_source_branch(repository, source_branch, local_ref)).to eq(false)
end
end
end
def create_remote_branch(repository, remote_name, branch_name, source_branch_name) def create_remote_branch(repository, remote_name, branch_name, source_branch_name)
source_branch = repository.branches.find { |branch| branch.name == source_branch_name } source_branch = repository.branches.find { |branch| branch.name == source_branch_name }
rugged = repository.rugged rugged = repository.rugged
......
...@@ -487,7 +487,7 @@ describe Gitlab::Shell do ...@@ -487,7 +487,7 @@ describe Gitlab::Shell do
it 'returns true when the command succeeds' do it 'returns true when the command succeeds' do
expect(Gitlab::Popen).to receive(:popen) expect(Gitlab::Popen).to receive(:popen)
.with([projects_path, 'rm-project', 'current/storage', 'project/path.git'], .with([projects_path, 'rm-project', 'current/storage', 'project/path.git'],
nil, popen_vars).and_return([nil, 0]) nil, popen_vars).and_return([nil, 0])
expect(gitlab_shell.remove_repository('current/storage', 'project/path')).to be true expect(gitlab_shell.remove_repository('current/storage', 'project/path')).to be true
end end
...@@ -495,7 +495,7 @@ describe Gitlab::Shell do ...@@ -495,7 +495,7 @@ describe Gitlab::Shell do
it 'returns false when the command fails' do it 'returns false when the command fails' do
expect(Gitlab::Popen).to receive(:popen) expect(Gitlab::Popen).to receive(:popen)
.with([projects_path, 'rm-project', 'current/storage', 'project/path.git'], .with([projects_path, 'rm-project', 'current/storage', 'project/path.git'],
nil, popen_vars).and_return(["error", 1]) nil, popen_vars).and_return(["error", 1])
expect(gitlab_shell.remove_repository('current/storage', 'project/path')).to be false expect(gitlab_shell.remove_repository('current/storage', 'project/path')).to be false
end end
......
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170927112318_update_legacy_diff_notes_type_for_import.rb')
describe UpdateLegacyDiffNotesTypeForImport, :migration do
let(:notes) { table(:notes) }
before do
notes.inheritance_column = nil
notes.create(type: 'Note')
notes.create(type: 'LegacyDiffNote')
notes.create(type: 'Github::Import::Note')
notes.create(type: 'Github::Import::LegacyDiffNote')
end
it 'updates the notes type' do
migrate!
expect(notes.pluck(:type))
.to contain_exactly('Note', 'Github::Import::Note', 'LegacyDiffNote', 'LegacyDiffNote')
end
end
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170927112319_update_notes_type_for_import.rb')
describe UpdateNotesTypeForImport, :migration do
let(:notes) { table(:notes) }
before do
notes.inheritance_column = nil
notes.create(type: 'Note')
notes.create(type: 'LegacyDiffNote')
notes.create(type: 'Github::Import::Note')
notes.create(type: 'Github::Import::LegacyDiffNote')
end
it 'updates the notes type' do
migrate!
expect(notes.pluck(:type))
.to contain_exactly('Note', 'Note', 'LegacyDiffNote', 'Github::Import::LegacyDiffNote')
end
end
require 'spec_helper' require 'spec_helper'
describe Ci::PipelineVariable, models: true do describe Ci::PipelineVariable do
subject { build(:ci_pipeline_variable) } subject { build(:ci_pipeline_variable) }
it { is_expected.to include_module(HasVariable) } it { is_expected.to include_module(HasVariable) }
......
...@@ -2860,6 +2860,12 @@ describe Project do ...@@ -2860,6 +2860,12 @@ describe Project do
end end
end end
describe '#hashed_storage?' do
it 'returns false' do
expect(project.hashed_storage?).to be_falsey
end
end
describe '#rename_repo' do describe '#rename_repo' do
before do before do
# Project#gitlab_shell returns a new instance of Gitlab::Shell on every # Project#gitlab_shell returns a new instance of Gitlab::Shell on every
......
require 'spec_helper' require 'spec_helper'
describe Repository, models: true do describe Repository do
include RepoHelpers include RepoHelpers
TestBlob = Struct.new(:path) TestBlob = Struct.new(:path)
...@@ -1693,27 +1693,41 @@ describe Repository, models: true do ...@@ -1693,27 +1693,41 @@ describe Repository, models: true do
end end
describe '#add_tag' do describe '#add_tag' do
context 'with a valid target' do let(:user) { build_stubbed(:user) }
let(:user) { build_stubbed(:user) }
it 'creates the tag using rugged' do shared_examples 'adding tag' do
expect(repository.rugged.tags).to receive(:create) context 'with a valid target' do
.with('8.5', repository.commit('master').id, it 'creates the tag' do
hash_including(message: 'foo', repository.add_tag(user, '8.5', 'master', 'foo')
tagger: hash_including(name: user.name, email: user.email)))
.and_call_original
repository.add_tag(user, '8.5', 'master', 'foo') tag = repository.find_tag('8.5')
end expect(tag).to be_present
expect(tag.message).to eq('foo')
expect(tag.dereferenced_target.id).to eq(repository.commit('master').id)
end
it 'returns a Gitlab::Git::Tag object' do it 'returns a Gitlab::Git::Tag object' do
tag = repository.add_tag(user, '8.5', 'master', 'foo') tag = repository.add_tag(user, '8.5', 'master', 'foo')
expect(tag).to be_a(Gitlab::Git::Tag)
end
end
expect(tag).to be_a(Gitlab::Git::Tag) context 'with an invalid target' do
it 'returns false' do
expect(repository.add_tag(user, '8.5', 'bar', 'foo')).to be false
end
end end
end
context 'when Gitaly operation_user_add_tag feature is enabled' do
it_behaves_like 'adding tag'
end
it 'passes commit SHA to pre-receive and update hooks,\ context 'when Gitaly operation_user_add_tag feature is disabled', skip_gitaly_mock: true do
and tag SHA to post-receive hook' do it_behaves_like 'adding tag'
it 'passes commit SHA to pre-receive and update hooks and tag SHA to post-receive hook' do
pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', project) pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', project)
update_hook = Gitlab::Git::Hook.new('update', project) update_hook = Gitlab::Git::Hook.new('update', project)
post_receive_hook = Gitlab::Git::Hook.new('post-receive', project) post_receive_hook = Gitlab::Git::Hook.new('post-receive', project)
...@@ -1738,12 +1752,6 @@ describe Repository, models: true do ...@@ -1738,12 +1752,6 @@ describe Repository, models: true do
.with(anything, anything, tag_sha, anything) .with(anything, anything, tag_sha, anything)
end end
end end
context 'with an invalid target' do
it 'returns false' do
expect(repository.add_tag(user, '8.5', 'bar', 'foo')).to be false
end
end
end end
describe '#rm_branch' do describe '#rm_branch' do
...@@ -1758,12 +1766,22 @@ describe Repository, models: true do ...@@ -1758,12 +1766,22 @@ describe Repository, models: true do
end end
describe '#rm_tag' do describe '#rm_tag' do
it 'removes a tag' do shared_examples 'removing tag' do
expect(repository).to receive(:before_remove_tag) it 'removes a tag' do
expect(repository).to receive(:before_remove_tag)
repository.rm_tag(create(:user), 'v1.1.0') repository.rm_tag(build_stubbed(:user), 'v1.1.0')
expect(repository.find_tag('v1.1.0')).to be_nil
end
end
context 'when Gitaly operation_user_delete_tag feature is enabled' do
it_behaves_like 'removing tag'
end
expect(repository.find_tag('v1.1.0')).to be_nil context 'when Gitaly operation_user_delete_tag feature is disabled', skip_gitaly_mock: true do
it_behaves_like 'removing tag'
end end
end end
......
require 'spec_helper'
describe UserCustomAttribute do
describe 'assocations' do
it { is_expected.to belong_to(:user) }
end
describe 'validations' do
subject { build :user_custom_attribute }
it { is_expected.to validate_presence_of(:user_id) }
it { is_expected.to validate_presence_of(:key) }
it { is_expected.to validate_presence_of(:value) }
it { is_expected.to validate_uniqueness_of(:key).scoped_to(:user_id) }
end
end
...@@ -44,6 +44,7 @@ describe User do ...@@ -44,6 +44,7 @@ describe User do
it { is_expected.to have_many(:chat_names).dependent(:destroy) } it { is_expected.to have_many(:chat_names).dependent(:destroy) }
it { is_expected.to have_many(:uploads).dependent(:destroy) } it { is_expected.to have_many(:uploads).dependent(:destroy) }
it { is_expected.to have_many(:reported_abuse_reports).dependent(:destroy).class_name('AbuseReport') } it { is_expected.to have_many(:reported_abuse_reports).dependent(:destroy).class_name('AbuseReport') }
it { is_expected.to have_many(:custom_attributes).class_name('UserCustomAttribute') }
describe "#abuse_report" do describe "#abuse_report" do
let(:current_user) { create(:user) } let(:current_user) { create(:user) }
......
...@@ -51,4 +51,18 @@ describe GlobalPolicy do ...@@ -51,4 +51,18 @@ describe GlobalPolicy do
end end
end end
end end
describe 'custom attributes' do
context 'regular user' do
it { is_expected.not_to be_allowed(:read_custom_attribute) }
it { is_expected.not_to be_allowed(:update_custom_attribute) }
end
context 'admin' do
let(:current_user) { create(:user, :admin) }
it { is_expected.to be_allowed(:read_custom_attribute) }
it { is_expected.to be_allowed(:update_custom_attribute) }
end
end
end end
...@@ -164,18 +164,25 @@ describe API::Helpers do ...@@ -164,18 +164,25 @@ describe API::Helpers do
end end
describe "when authenticating using a user's private token" do describe "when authenticating using a user's private token" do
it "returns nil for an invalid token" do it "returns a 401 response for an invalid token" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token' env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false } allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false }
expect(current_user).to be_nil expect { current_user }.to raise_error /401/
end end
it "returns nil for a user without access" do it "returns a 401 response for a user without access" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
expect(current_user).to be_nil expect { current_user }.to raise_error /401/
end
it 'returns a 401 response for a user who is blocked' do
user.block!
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
expect { current_user }.to raise_error /401/
end end
it "leaves user as is when sudo not specified" do it "leaves user as is when sudo not specified" do
...@@ -198,24 +205,31 @@ describe API::Helpers do ...@@ -198,24 +205,31 @@ describe API::Helpers do
allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false } allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false }
end end
it "returns nil for an invalid token" do it "returns a 401 response for an invalid token" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token' env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
expect(current_user).to be_nil expect { current_user }.to raise_error /401/
end end
it "returns nil for a user without access" do it "returns a 401 response for a user without access" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
expect(current_user).to be_nil expect { current_user }.to raise_error /401/
end
it 'returns a 401 response for a user who is blocked' do
user.block!
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect { current_user }.to raise_error /401/
end end
it "returns nil for a token without the appropriate scope" do it "returns a 401 response for a token without the appropriate scope" do
personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user']) personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to be_nil expect { current_user }.to raise_error /401/
end end
it "leaves user as is when sudo not specified" do it "leaves user as is when sudo not specified" do
...@@ -231,14 +245,14 @@ describe API::Helpers do ...@@ -231,14 +245,14 @@ describe API::Helpers do
personal_access_token.revoke! personal_access_token.revoke!
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to be_nil expect { current_user }.to raise_error /401/
end end
it 'does not allow expired tokens' do it 'does not allow expired tokens' do
personal_access_token.update_attributes!(expires_at: 1.day.ago) personal_access_token.update_attributes!(expires_at: 1.day.ago)
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to be_nil expect { current_user }.to raise_error /401/
end end
end end
...@@ -385,6 +399,18 @@ describe API::Helpers do ...@@ -385,6 +399,18 @@ describe API::Helpers do
end end
end end
end end
context 'when user is blocked' do
before do
user.block!
end
it 'changes current_user to sudo' do
set_env(admin, user.id)
expect(current_user).to eq(user)
end
end
end end
context 'with regular user' do context 'with regular user' do
...@@ -524,11 +550,10 @@ describe API::Helpers do ...@@ -524,11 +550,10 @@ describe API::Helpers do
context 'current_user is nil' do context 'current_user is nil' do
before do before do
expect_any_instance_of(self.class).to receive(:current_user).and_return(nil) expect_any_instance_of(self.class).to receive(:current_user).and_return(nil)
allow_any_instance_of(self.class).to receive(:initial_current_user).and_return(nil)
end end
it 'returns a 401 response' do it 'returns a 401 response' do
expect { authenticate! }.to raise_error '401 - {"message"=>"401 Unauthorized"}' expect { authenticate! }.to raise_error /401/
end end
end end
...@@ -536,35 +561,12 @@ describe API::Helpers do ...@@ -536,35 +561,12 @@ describe API::Helpers do
let(:user) { build(:user) } let(:user) { build(:user) }
before do before do
expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(user) expect_any_instance_of(self.class).to receive(:current_user).and_return(user)
expect_any_instance_of(self.class).to receive(:initial_current_user).and_return(user)
end end
it 'does not raise an error' do it 'does not raise an error' do
expect { authenticate! }.not_to raise_error expect { authenticate! }.not_to raise_error
end end
end end
context 'current_user is blocked' do
let(:user) { build(:user, :blocked) }
before do
expect_any_instance_of(self.class).to receive(:current_user).at_least(:once).and_return(user)
end
it 'raises an error' do
expect_any_instance_of(self.class).to receive(:initial_current_user).and_return(user)
expect { authenticate! }.to raise_error '401 - {"message"=>"401 Unauthorized"}'
end
it "doesn't raise an error if an admin user is impersonating a blocked user (via sudo)" do
admin_user = build(:user, :admin)
expect_any_instance_of(self.class).to receive(:initial_current_user).and_return(admin_user)
expect { authenticate! }.not_to raise_error
end
end
end end
end end
...@@ -203,10 +203,9 @@ describe API::Users do ...@@ -203,10 +203,9 @@ describe API::Users do
context "when authenticated and ldap is enabled" do context "when authenticated and ldap is enabled" do
it "returns non-ldap user" do it "returns non-ldap user" do
User.delete_all
create :omniauth_user, provider: "ldapserver1" create :omniauth_user, provider: "ldapserver1"
get api("/users", user), skip_ldap: "true" get api("/users", user), skip_ldap: "true"
expect(response.status).to eq 200 expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array expect(json_response).to be_an Array
username = user.username username = user.username
expect(json_response.first["username"]).to eq username expect(json_response.first["username"]).to eq username
...@@ -1939,4 +1938,8 @@ describe API::Users do ...@@ -1939,4 +1938,8 @@ describe API::Users do
expect(impersonation_token.reload.revoked).to be_truthy expect(impersonation_token.reload.revoked).to be_truthy
end end
end end
include_examples 'custom attributes endpoints', 'users' do
let(:attributable) { user }
end
end end
...@@ -11,5 +11,16 @@ describe MergeRequests::PostMergeService do ...@@ -11,5 +11,16 @@ describe MergeRequests::PostMergeService do
describe '#execute' do describe '#execute' do
it_behaves_like 'cache counters invalidator' it_behaves_like 'cache counters invalidator'
it 'refreshes the number of open merge requests for a valid MR', :use_clean_rails_memory_store_caching do
# Cache the counter before the MR changed state.
project.open_merge_requests_count
merge_request.update!(state: 'merged')
service = described_class.new(project, user, {})
expect { service.execute(merge_request) }
.to change { project.open_merge_requests_count }.from(1).to(0)
end
end end
end end
...@@ -28,7 +28,7 @@ describe Tags::CreateService do ...@@ -28,7 +28,7 @@ describe Tags::CreateService do
it 'returns an error' do it 'returns an error' do
expect(repository).to receive(:add_tag) expect(repository).to receive(:add_tag)
.with(user, 'v1.1.0', 'master', 'Foo') .with(user, 'v1.1.0', 'master', 'Foo')
.and_raise(Rugged::TagError) .and_raise(Gitlab::Git::Repository::TagExistsError)
response = service.execute('v1.1.0', 'master', 'Foo') response = service.execute('v1.1.0', 'master', 'Foo')
......
shared_examples 'custom attributes endpoints' do |attributable_name|
let!(:custom_attribute1) { attributable.custom_attributes.create key: 'foo', value: 'foo' }
let!(:custom_attribute2) { attributable.custom_attributes.create key: 'bar', value: 'bar' }
describe "GET /#{attributable_name} with custom attributes filter" do
let!(:other_attributable) { create attributable.class.name.underscore }
context 'with an unauthorized user' do
it 'does not filter by custom attributes' do
get api("/#{attributable_name}", user), custom_attributes: { foo: 'foo', bar: 'bar' }
expect(response).to have_http_status(200)
expect(json_response.size).to be 2
end
end
it 'filters by custom attributes' do
get api("/#{attributable_name}", admin), custom_attributes: { foo: 'foo', bar: 'bar' }
expect(response).to have_http_status(200)
expect(json_response.size).to be 1
expect(json_response.first['id']).to eq attributable.id
end
end
describe "GET /#{attributable_name}/:id/custom_attributes" do
context 'with an unauthorized user' do
subject { get api("/#{attributable_name}/#{attributable.id}/custom_attributes", user) }
it_behaves_like 'an unauthorized API user'
end
it 'returns all custom attributes' do
get api("/#{attributable_name}/#{attributable.id}/custom_attributes", admin)
expect(response).to have_http_status(200)
expect(json_response).to contain_exactly(
{ 'key' => 'foo', 'value' => 'foo' },
{ 'key' => 'bar', 'value' => 'bar' }
)
end
end
describe "GET /#{attributable_name}/:id/custom_attributes/:key" do
context 'with an unauthorized user' do
subject { get api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", user) }
it_behaves_like 'an unauthorized API user'
end
it 'returns a single custom attribute' do
get api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin)
expect(response).to have_http_status(200)
expect(json_response).to eq({ 'key' => 'foo', 'value' => 'foo' })
end
end
describe "PUT /#{attributable_name}/:id/custom_attributes/:key" do
context 'with an unauthorized user' do
subject { put api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", user), value: 'new' }
it_behaves_like 'an unauthorized API user'
end
it 'creates a new custom attribute' do
expect do
put api("/#{attributable_name}/#{attributable.id}/custom_attributes/new", admin), value: 'new'
end.to change { attributable.custom_attributes.count }.by(1)
expect(response).to have_http_status(200)
expect(json_response).to eq({ 'key' => 'new', 'value' => 'new' })
expect(attributable.custom_attributes.find_by(key: 'new').value).to eq 'new'
end
it 'updates an existing custom attribute' do
expect do
put api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin), value: 'new'
end.not_to change { attributable.custom_attributes.count }
expect(response).to have_http_status(200)
expect(json_response).to eq({ 'key' => 'foo', 'value' => 'new' })
expect(custom_attribute1.reload.value).to eq 'new'
end
end
describe "DELETE /#{attributable_name}/:id/custom_attributes/:key" do
context 'with an unauthorized user' do
subject { delete api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", user) }
it_behaves_like 'an unauthorized API user'
end
it 'deletes an existing custom attribute' do
expect do
delete api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin)
end.to change { attributable.custom_attributes.count }.by(-1)
expect(response).to have_http_status(204)
expect(attributable.custom_attributes.find_by(key: 'foo')).to be_nil
end
end
end
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