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 @@
karma.config.js
webpack.config.js
svg.config.js
/app/assets/javascripts/locale/**/*.js
/app/assets/javascripts/locale/**/app.js
......@@ -40,10 +40,10 @@ export default () => {
class="text-center"
v-if="error">
<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 v-else>
An error occured whilst parsing the file.
An error occurred whilst parsing the file.
</span>
</p>
</div>
......
......@@ -48,10 +48,10 @@ export default () => {
class="text-center"
v-if="error">
<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 v-else>
An error occured whilst decoding the file.
An error occurred whilst decoding the file.
</span>
</p>
</div>
......
......@@ -96,7 +96,7 @@ export default {
<div class="flash-container"
v-if="error">
<div class="flash-alert">
An error occured. Please try again.
An error occurred. Please try again.
</div>
</div>
<label class="label-light"
......
......@@ -167,7 +167,7 @@ window.Build = (function () {
Build.prototype.getBuildTrace = function () {
return $.ajax({
url: `${this.pageUrl}/trace.json`,
data: this.state,
data: { state: this.state },
})
.done((log) => {
setCiStatusFavicon(`${this.pageUrl}/status.json`);
......
......@@ -191,7 +191,7 @@ export default {
this.service.postAction(endpoint)
.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 {
this.service.postAction(endpoint)
.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 @@
* causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a
*/
import Cookies from 'js-cookie';
const LINE_NUMBER_CLASS = 'diff-line-num';
const UNFOLDABLE_LINE_CLASS = 'js-unfold';
const NO_COMMENT_CLASS = 'no-comment-btn';
......@@ -27,9 +29,7 @@ export default {
this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === '';
}
if (typeof notes !== 'undefined' && !this.isParallelView) {
this.isParallelView = notes.isParallelView && notes.isParallelView();
}
this.isParallelView = Cookies.get('diff_view') === 'parallel';
if (this.userCanCreateNote) {
$diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e))
......
......@@ -14,7 +14,7 @@ class DropdownEmoji extends gl.FilteredSearchDropdown {
loadingTemplate: this.loadingTemplate,
onError() {
/* 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 */
},
},
......
......@@ -17,7 +17,7 @@ class DropdownNonUser extends gl.FilteredSearchDropdown {
preprocessing,
onError() {
/* 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 */
},
},
......
......@@ -27,7 +27,7 @@ class DropdownUser extends gl.FilteredSearchDropdown {
},
onError() {
/* 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 */
},
},
......
......@@ -44,7 +44,7 @@ class FilteredSearchManager {
.catch((error) => {
if (error.name === 'RecentSearchesServiceError') return undefined;
// 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
return [];
})
......
......@@ -16,9 +16,8 @@ const locales = allLocales.reduce((d, obj) => {
return data;
}, {});
let lang = document.querySelector('html').getAttribute('lang') || 'en';
lang = lang.replace(/-/g, '_');
const langAttribute = document.querySelector('html').getAttribute('lang');
const lang = (langAttribute || 'en').replace(/-/g, '_');
const locale = new Jed(locales[lang]);
/**
......
......@@ -311,7 +311,10 @@ $(function () {
return $container.remove();
// 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
$body.on('click', '.js-toggle-diff-comments', function (e) {
var $this = $(this);
......
......@@ -97,7 +97,7 @@ export default {
postAction(endpoint) {
this.service.postAction(endpoint)
.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';
aspectRatio: 1,
modal: true,
scalable: false,
rotatable: false,
rotatable: true,
checkOrientation: true,
zoomable: true,
dragMode: 'move',
guides: false,
......
......@@ -37,14 +37,14 @@ export default {
content: f.newContent,
}));
const payload = {
branch: Store.targetBranch,
branch: Store.currentBranch,
commit_message: commitMessage,
actions,
};
Store.submitCommitsLoading = true;
Service.commitFiles(payload)
.then(this.resetCommitState)
.catch(() => Flash('An error occured while committing your changes'));
.catch(() => Flash('An error occurred while committing your changes'));
},
resetCommitState() {
......@@ -105,7 +105,7 @@ export default {
</label>
<div class="col-md-6">
<span class="help-block">
{{targetBranch}}
{{currentBranch}}
</span>
</div>
</div>
......
......@@ -26,16 +26,6 @@ export default {
this.editMode = !this.editMode;
Store.toggleBlobView();
},
toggleProjectRefsForm() {
$('.project-refs-form').toggleClass('disabled', this.editMode);
$('.js-tree-ref-target-holder').toggle(this.editMode);
},
},
watch: {
editMode() {
this.toggleProjectRefsForm();
},
},
};
</script>
......
......@@ -49,7 +49,7 @@ export default {
v-else
class="vertical-center render-error">
<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>
</div>
</div>
......
......@@ -37,17 +37,24 @@ export default {
let file = clickedFile;
if (file.loading) return;
file.loading = true;
if (file.type === 'tree' && file.opened) {
file = Store.removeChildFilesOfTree(file);
file.loading = false;
} else {
Service.url = file.url;
Helper.getContent(file)
.then(() => {
file.loading = false;
Helper.scrollTabsRight();
})
.catch(Helper.loadingError);
const openFile = Helper.getFileFromPath(file.url);
if (openFile) {
file.loading = false;
Store.setActiveFiles(openFile);
} else {
Service.url = file.url;
Helper.getContent(file)
.then(() => {
file.loading = false;
Helper.scrollTabsRight();
})
.catch(Helper.loadingError);
}
}
},
......
......@@ -263,6 +263,10 @@ const RepoHelper = {
return Store.openedFiles.find(openedFile => Store.activeFile.url === openedFile.url);
},
getFileFromPath(path) {
return Store.openedFiles.find(file => file.url === path);
},
loadingError() {
Flash('Unable to load this content at this time.');
},
......
......@@ -11,10 +11,6 @@ function initDropdowns() {
}
function addEventsForNonVueEls() {
$(document).on('change', '.dropdown', () => {
Store.targetBranch = $('.project-refs-target-form input[name="ref"]').val();
});
window.onbeforeunload = function confirmUnload(e) {
const hasChanged = Store.openedFiles
.some(file => file.changed);
......
......@@ -32,7 +32,6 @@ const RepoStore = {
isCommitable: false,
binary: false,
currentBranch: '',
targetBranch: 'new-branch',
commitMessage: '',
binaryTypes: {
png: false,
......
......@@ -43,6 +43,8 @@ import Cookies from 'js-cookie';
$allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right');
$('aside.right-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) {
return Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
......
......@@ -38,7 +38,7 @@ class SidebarMoveIssue {
data: (searchTerm, callback) => {
this.mediator.fetchAutocompleteProjects(searchTerm)
.then(callback)
.catch(() => new Flash('An error occured while fetching projects autocomplete.'));
.catch(() => new Flash('An error occurred while fetching projects autocomplete.'));
},
renderRow: project => `
<li>
......@@ -73,7 +73,7 @@ class SidebarMoveIssue {
this.mediator.moveIssue()
.catch(() => {
Flash('An error occured while moving the issue.');
Flash('An error occurred while moving the issue.');
this.$confirmButton
.enable()
.removeClass('is-loading');
......
......@@ -41,7 +41,7 @@ export default class SidebarMediator {
this.store.setAssigneeData(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) {
......
......@@ -535,7 +535,6 @@
}
.diff-notes-collapse {
position: relative;
width: 19px;
height: 19px;
padding: 0;
......@@ -543,11 +542,7 @@
z-index: 100;
svg {
position: absolute;
left: 50%;
top: 50%;
margin-left: -5.5px;
margin-top: -5.5px;
vertical-align: text-top;
}
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 @@
#
class UsersFinder
include CreatedAtFilter
include CustomAttributesFilter
attr_accessor :current_user, :params
......@@ -33,6 +34,7 @@ class UsersFinder
users = by_external_identity(users)
users = by_external(users)
users = by_created_at(users)
users = by_custom_attributes(users)
users = by_non_ldap(users)
users
......
......@@ -10,11 +10,7 @@ module BreadcrumbsHelper
def breadcrumb_title_link
return @breadcrumb_link if @breadcrumb_link
if controller.available_action?(:index)
url_for(action: "index")
else
request.path
end
request.path
end
def breadcrumb_title(title)
......
......@@ -249,16 +249,25 @@ module IssuablesHelper
Gitlab::IssuablesCountForState.new(finder)[state]
end
def close_issuable_url(issuable)
issuable_url(issuable, close_reopen_params(issuable, :close))
def close_issuable_path(issuable)
issuable_path(issuable, close_reopen_params(issuable, :close))
end
def reopen_issuable_url(issuable)
issuable_url(issuable, close_reopen_params(issuable, :reopen))
def reopen_issuable_path(issuable)
issuable_path(issuable, close_reopen_params(issuable, :reopen))
end
def close_reopen_issuable_url(issuable, should_inverse = false)
issuable.closed? ^ should_inverse ? reopen_issuable_url(issuable) : close_issuable_url(issuable)
def close_reopen_issuable_path(issuable, should_inverse = false)
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
def issuable_url(issuable, *options)
......
......@@ -496,13 +496,7 @@ class Repository
def exists?
return false unless full_path
Gitlab::GitalyClient.migrate(:repository_exists) do |enabled|
if enabled
raw_repository.exists?
else
refs_directory_exists?
end
end
raw_repository.exists?
end
cache_method :exists?
......@@ -1143,12 +1137,6 @@ class Repository
blob.data
end
def refs_directory_exists?
circuit_breaker.perform do
File.exist?(File.join(path_to_repo, 'refs'))
end
end
def cache
# TODO: should we use UUIDs here? We could move repositories without clearing this cache
@cache ||= RepositoryCache.new(full_path, @project.id)
......@@ -1200,10 +1188,6 @@ class Repository
Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git', Gitlab::GlRepository.gl_repository(project, false))
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)
ref ||= root_ref
......
......@@ -133,6 +133,8 @@ class User < ActiveRecord::Base
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 :custom_attributes, class_name: 'UserCustomAttribute'
#
# 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
rule { ~(anonymous & restricted_public_level) }.policy do
enable :read_users_list
end
rule { admin }.policy do
enable :read_custom_attribute
enable :update_custom_attribute
end
end
......@@ -14,6 +14,7 @@ module MergeRequests
notification_service.merge_mr(merge_request, current_user)
execute_hooks(merge_request, 'merge')
invalidate_cache_counts(merge_request, users: merge_request.assignees)
merge_request.update_project_counter_caches
end
private
......
......@@ -11,7 +11,7 @@ module Tags
begin
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")
rescue Gitlab::Git::HooksService::PreReceiveError => ex
return error(ex.message)
......
......@@ -12,10 +12,10 @@ module Users
def execute(validate: true, &block)
yield(@user) if block_given?
assign_attributes(&block)
user_exists = @user.persisted?
assign_attributes(&block)
if @user.save(validate: validate)
notify_success(user_exists)
else
......@@ -31,7 +31,7 @@ module Users
true
end
protected
private
def notify_success(user_exists)
notify_new_user(@user, nil) unless user_exists
......@@ -39,8 +39,6 @@ module Users
success
end
private
def assign_attributes(&block)
if @user.user_synced_attributes_metadata
params.except!(*@user.user_synced_attributes_metadata.read_only_attributes)
......
- confirmation_link = confirmation_url(@resource, confirmation_token: @token)
- if @resource.unconfirmed_email.present?
#content
= email_default_heading(@resource.unconfirmed_email)
%p Click the link below to confirm your email address.
#cta
= link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token)
= link_to confirmation_link, confirmation_link
- else
#content
- if Gitlab.com?
......@@ -12,4 +13,4 @@
= email_default_heading("Welcome, #{@resource.name}!")
%p To get started, click the link below to confirm your account.
#cta
= link_to 'Confirm your account', confirmation_url(@resource, confirmation_token: @token)
= link_to confirmation_link, confirmation_link
......@@ -26,19 +26,19 @@
= link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('search')
- 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
= custom_icon('issues')
- issues_count = assigned_issuables_count(:issues)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= 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
= custom_icon('mr_bold')
- merge_requests_count = assigned_issuables_count(:merge_requests)
%span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= 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
= custom_icon('todo_done')
%span.badge.todos-count{ class: ('hidden' if todos_pending_count.zero?) }
......
......@@ -16,5 +16,5 @@
= breadcrumb_list_item link_to(extra[:text], extra[:link])
= render "layouts/nav/breadcrumbs/collapsed_dropdown", location: :after
%li
%h2.breadcrumbs-sub-title= @breadcrumb_title
%h2.breadcrumbs-sub-title= link_to @breadcrumb_title, breadcrumb_title_link
= yield :header_content
- access = note_max_access_for_user(note)
- 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
- if access.nonzero?
%span.note-role.note-role-access= Gitlab::Access.human_access(access)
......
.tree-ref-container
.tree-ref-holder
= 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?
= 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 @@
- button_method = issuable_close_reopen_button_method(issuable)
- 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}"
= 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}"
- elsif can_update && !is_current_user
= render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
......
......@@ -7,7 +7,7 @@
- button_method = issuable_close_reopen_button_method(issuable)
.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}"
= button_tag type: 'button', class: "#{toggle_class} btn-#{button_action}-color",
......@@ -16,7 +16,7 @@
%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'}",
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.btn.btn-transparent
= icon('check', class: 'icon')
......@@ -26,7 +26,7 @@
= display_issuable_type
%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.btn.btn-transparent
= 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 @@
#
# 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
enable_extension "plpgsql"
......@@ -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", ["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", "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_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
......@@ -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
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|
t.boolean "name_synced", default: false
t.boolean "email_synced", default: false
......@@ -2160,6 +2172,7 @@ ActiveRecord::Schema.define(version: 20170921115009) do
add_foreign_key "todos", "projects", name: "fk_45054f9c45", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade
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 "users_star_projects", "projects", name: "fk_22cd27ddfc", on_delete: :cascade
add_foreign_key "web_hook_logs", "web_hooks", on_delete: :cascade
......
......@@ -14,6 +14,7 @@ following locations:
- [Project-level Variables](project_level_variables.md)
- [Group-level Variables](group_level_variables.md)
- [Commits](commits.md)
- [Custom Attributes](custom_attributes.md)
- [Deployments](deployments.md)
- [Deploy Keys](deploy_keys.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
| 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 |
| `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_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:
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
Get a single user.
......
......@@ -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
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. Check whether the `overlay` module is loaded:
......@@ -271,14 +273,27 @@ which can be avoided if a different driver is used, for example `overlay2`.
overlay
```
1. Use the driver by defining a variable at the top of your `.gitlab-ci.yml`:
### Use driver per project
```
variables:
DOCKER_DRIVER: overlay2
```
> **Note:**
You can enable the driver for each project individually by editing the project's `.gitlab-ci.yml`:
```
variables:
DOCKER_DRIVER: overlay2
```
### 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
[Use the OverlayFS storage driver](https://docs.docker.com/engine/userguide/storagedriver/overlayfs-driver/).
......
......@@ -154,6 +154,16 @@ always in-sync with the codebase.
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?
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.
---
### [Illustrations](illustrations.md)
Guidelines for principals and styles related to illustrations for GitLab.
---
### [Copy](copy.md)
Conventions on text and messaging within labels, buttons, and other components.
......
# 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/).
- **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
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
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
>**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)
and [problems](https://bugs.mysql.com/bug.php?id=65830) that
[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).
- We recommend using MySQL version 5.6 or later. Please see the following [issue][ce-38152].
## Initial database setup
......@@ -13,7 +14,7 @@ and [problems](https://bugs.mysql.com/bug.php?id=65830) that
# Install the database packages
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
# 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
[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
[ce-38152]: https://gitlab.com/gitlab-org/gitlab-ce/issues/38152
---
last_updated: 2017-09-25
---
# GitLab Kubernetes / OpenShift integration
GitLab can be configured to interact with Kubernetes, or other systems using the
......@@ -6,10 +10,6 @@ Kubernetes API (such as OpenShift).
Each project can be configured to connect to a different Kubernetes cluster, see
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
Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
......@@ -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)
(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
The Kubernetes service exposes following
The Kubernetes service exposes the following
[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_TOKEN`
- `KUBE_URL` - Equal to the API URL.
- `KUBE_TOKEN` - The Kubernetes token.
- `KUBE_NAMESPACE` - The Kubernetes namespace is auto-generated if not specified.
The default value is `<project_name>-<project_id>`. You can overwrite it to
use different one if needed, otherwise the `KUBE_NAMESPACE` variable will
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.
- `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.
- `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.
## 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:**
Added 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
Introduced in GitLab 8.15. You must be the project owner or have `master` permissions
to use terminals. Support is limited to the first container in the
first pod of your environment.
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
containers. To use this integration, you should deploy to Kubernetes using
the deployment variables above, ensuring any pods you create are labelled with
`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest!
[ee]: https://about.gitlab.com/gitlab-ee/
# Monitoring AWS Resources
> [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.
## Requirements
The [Prometheus service](../prometheus/index.md) must be enabled.
## Metrics supported
| Name | Query |
......
......@@ -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.
## Requirements
The [Prometheus service](../prometheus/index.md) must be enabled.
## Metrics supported
| Name | Query |
......
# Monitoring Kubernetes
> [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
......@@ -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
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
> [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.
## Requirements
The [Prometheus service](../prometheus/index.md) must be enabled.
## Metrics supported
| Name | Query |
......
# Monitoring NGINX Ingress Controller
> [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.
## Requirements
The [Prometheus service](../prometheus/index.md) must be enabled.
## Metrics supported
| Name | Query |
......
......@@ -62,7 +62,7 @@ which is highly recommendable and much faster than hardcoding.
If you set up a GitLab Pages project on GitLab.com,
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,
or the group name you created this project under.
......@@ -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
(`*.gitlab.io`) with your own.
Learn more about [namespaces](../../group/index.md#namespaces).
### Practical examples
#### Project Websites
......
---
last_updated: 2017-09-28
---
# 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 ||
> **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 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
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
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
`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`.
### 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
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:
- 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
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
that is created on GitLab looks like and the website URL it will be ultimately
be served on.
......@@ -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`
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
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
gives you absolute control over the build process. You can actually watch your
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:**
> Before reading this section, make sure you familiarize yourself with GitLab CI
> 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:
### 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
see the **New Domain** button when visiting your project's settings through the
gear icon in the top right and then navigating to **Pages**.
......@@ -349,6 +359,9 @@ private key when adding a new domain.
![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
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:
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
When using Pages under the general domain of a GitLab instance (`*.example.io`),
......
......@@ -77,7 +77,7 @@ module API
raise RevokedError
when AccessTokenValidationService::VALID
@current_user = User.find(access_token.resource_owner_id)
User.find(access_token.resource_owner_id)
end
end
......@@ -86,7 +86,13 @@ module API
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
def find_user_by_job_token
......@@ -99,10 +105,6 @@ module API
end
end
def current_user
@current_user
end
private
def route_authentication_setting
......@@ -125,7 +127,16 @@ module API
end
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
def doorkeeper_request
......@@ -187,6 +198,7 @@ module API
TokenNotFoundError = Class.new(StandardError)
ExpiredError = Class.new(StandardError)
RevokedError = Class.new(StandardError)
UnauthorizedError = Class.new(StandardError)
class InsufficientScopeError < StandardError
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
expose :failing_on_hosts
expose :total_failures
end
class CustomAttribute < Grape::Entity
expose :key
expose :value
end
end
end
......@@ -5,6 +5,8 @@ module API
include Gitlab::Utils
include Helpers::Pagination
UnauthorizedError = Class.new(StandardError)
SUDO_HEADER = "HTTP_SUDO".freeze
SUDO_PARAM = :sudo
......@@ -159,7 +161,7 @@ module API
end
def authenticate!
unauthorized! unless current_user && can?(initial_current_user, :access_api)
unauthorized! unless current_user
end
def authenticate_non_get!
......@@ -422,18 +424,25 @@ module API
def initial_current_user
return @initial_current_user if defined?(@initial_current_user)
Gitlab::Auth::UniqueIpsLimiter.limit_user! do
@initial_current_user ||= find_user_by_private_token(scopes: scopes_registered_for_endpoint)
@initial_current_user ||= doorkeeper_guard(scopes: scopes_registered_for_endpoint)
@initial_current_user ||= find_user_from_warden
@initial_current_user ||= find_user_by_job_token
begin
@initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user }
rescue APIGuard::UnauthorizedError, UnauthorizedError
unauthorized!
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?
@initial_current_user = nil
end
return nil unless user
@initial_current_user
end
raise UnauthorizedError unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
user
end
def sudo!
......
......@@ -6,6 +6,8 @@ module API
allow_access_with_scope :read_user, if: -> (request) { request.get? }
resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
include CustomAttributesEndpoints
before do
authenticate_non_get!
end
......@@ -332,7 +334,7 @@ module API
user = User.find_by(id: params.delete(:id))
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?
NotificationService.new.new_email(email)
......
require_relative 'error'
require_relative 'import/issue'
require_relative 'import/legacy_diff_note'
require_relative 'import/merge_request'
require_relative 'import/note'
module Github
class Import
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,
: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
GitError = Class.new(StandardError)
DeleteBranchError = Class.new(StandardError)
CreateTreeError = Class.new(StandardError)
TagExistsError = Class.new(StandardError)
class << self
# Unlike `new`, `create` takes the repository path
......@@ -70,8 +71,6 @@ module Gitlab
delegate :empty?,
to: :rugged
delegate :exists?, to: :gitaly_repository_client
def ==(other)
path == other.path
end
......@@ -99,6 +98,18 @@ module Gitlab
@circuit_breaker ||= Gitlab::Git::Storage::CircuitBreaker.for_storage(storage)
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
# sorted by name ASC
def branch_names
......@@ -633,24 +644,13 @@ module Gitlab
end
def 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)
}
gitaly_migrate(:operation_user_add_tag) do |is_enabled|
if is_enabled
gitaly_add_tag(tag_name, user: user, target: target, message: message)
else
rugged_add_tag(tag_name, user: user, target: target, message: message)
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
def rm_branch(branch_name, user:)
......@@ -658,7 +658,13 @@ module Gitlab
end
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
def find_tag(name)
......@@ -934,7 +940,11 @@ module Gitlab
if start_repository == self
yield commit(start_branch_name)
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)
yield branch_commit
......@@ -963,8 +973,9 @@ module Gitlab
with_repo_branch_commit(source_repository, source_branch) do |commit|
if commit
write_ref(local_ref, commit.sha)
true
else
raise Rugged::ReferenceError, 'source repository is empty'
false
end
end
end
......@@ -1030,6 +1041,10 @@ module Gitlab
Gitlab::GitalyClient::Util.repository(@storage, @relative_path)
end
def gitaly_operations_client
@gitaly_operations_client ||= Gitlab::GitalyClient::OperationService.new(self)
end
def gitaly_ref_client
@gitaly_ref_client ||= Gitlab::GitalyClient::RefService.new(self)
end
......@@ -1364,6 +1379,33 @@ module Gitlab
false
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)
rugged_ref = rugged.branches.create(ref, start_point)
target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
......
......@@ -19,6 +19,7 @@ module Gitlab
command_not_allowed: "The command you're trying to execute 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.',
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."
}.freeze
......@@ -172,7 +173,7 @@ module Gitlab
# TODO: please clean this up
def check_push_access!(changes)
if project.repository_read_only?
raise UnauthorizedError, 'The repository is temporarily read-only. Please try again later.'
raise UnauthorizedError, ERROR_MESSAGES[:readonly]
end
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
def consume_tags_response(response)
response.flat_map do |message|
message.tags.map do |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
message.tags.map { |gitaly_tag| Util.gitlab_tag_from_gitaly_tag(@repository, gitaly_tag) }
end
end
......
......@@ -10,6 +10,30 @@ module Gitlab
git_alternate_object_directories: Array.wrap(Gitlab::Git::Env['GIT_ALTERNATE_OBJECT_DIRECTORIES'])
)
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
......
......@@ -1523,7 +1523,7 @@ msgstr ""
msgid "There are problems accessing Git storage: "
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 ""
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
set_recent_searches(project_1_local_storage_key, 'fail')
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
......@@ -131,6 +131,14 @@ describe 'Issues' do
end
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
issue = create(:issue, author: user, assignees: [user], project: project, title: 'foobar')
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
expect(current_path).to include('one/two/three-test')
expect(find('.wiki-pages')).to have_content('Three')
click_on('Three')
first(:link, text: 'Three').click
expect(find('.nav-text')).to have_content('Three')
......
......@@ -34,7 +34,7 @@ describe 'User views a wiki page' do
it 'shows the history of a page that has a path', :js do
expect(current_path).to include('one/two/three-test')
click_on('Three')
first(:link, text: 'Three').click
click_on('Page history')
expect(current_path).to include('one/two/three-test')
......@@ -48,7 +48,7 @@ describe 'User views a wiki page' do
expect(current_path).to include('one/two/three-test')
expect(find('.wiki-pages')).to have_content('Three')
click_on('Three')
first(:link, text: 'Three').click
expect(find('.nav-text')).to have_content('Three')
......
......@@ -35,15 +35,30 @@ feature 'Master deletes tag' do
end
context 'when pre-receive hook fails', js: true do
before do
allow_any_instance_of(Gitlab::Git::HooksService).to receive(:execute)
.and_raise(Gitlab::Git::HooksService::PreReceiveError, 'Do not delete tags')
context 'when Gitaly operation_user_delete_tag feature is enabled' do
before do
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
scenario 'shows the error message' do
delete_first_tag
context 'when Gitaly operation_user_delete_tag feature is disabled', skip_gitaly_mock: true do
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
......
......@@ -72,6 +72,15 @@ describe UsersFinder do
expect(users.map(&:username)).not_to include([filtered_user_before.username, filtered_user_after.username])
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
context 'with an admin user' do
......@@ -88,6 +97,19 @@ describe UsersFinder do
expect(users).to contain_exactly(admin, user1, user2, external_user, omniauth_user)
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
......@@ -117,7 +117,7 @@ describe('iPython notebook renderer', () => {
it('shows error message', () => {
expect(
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', () => {
it('shows error message', () => {
expect(
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', () => {
it('shows error message', () => {
expect(
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', () => {
});
});
});
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', () => {
expect(vm.$el.textContent).toMatch('Edit');
spyOn(vm, 'editCancelClicked').and.callThrough();
spyOn(vm, 'toggleProjectRefsForm');
vm.$el.click();
Vue.nextTick(() => {
expect(vm.editCancelClicked).toHaveBeenCalled();
expect(vm.toggleProjectRefsForm).toHaveBeenCalled();
expect(vm.$el.textContent).toMatch('Cancel edit');
done();
});
......
......@@ -79,6 +79,20 @@ describe('RepoSidebar', () => {
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', () => {
spyOn(RepoStore, 'removeChildFilesOfTree').and.callThrough();
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
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)
source_branch = repository.branches.find { |branch| branch.name == source_branch_name }
rugged = repository.rugged
......
......@@ -487,7 +487,7 @@ describe Gitlab::Shell do
it 'returns true when the command succeeds' do
expect(Gitlab::Popen).to receive(:popen)
.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
end
......@@ -495,7 +495,7 @@ describe Gitlab::Shell do
it 'returns false when the command fails' do
expect(Gitlab::Popen).to receive(:popen)
.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
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'
describe Ci::PipelineVariable, models: true do
describe Ci::PipelineVariable do
subject { build(:ci_pipeline_variable) }
it { is_expected.to include_module(HasVariable) }
......
......@@ -2860,6 +2860,12 @@ describe Project do
end
end
describe '#hashed_storage?' do
it 'returns false' do
expect(project.hashed_storage?).to be_falsey
end
end
describe '#rename_repo' do
before do
# Project#gitlab_shell returns a new instance of Gitlab::Shell on every
......
require 'spec_helper'
describe Repository, models: true do
describe Repository do
include RepoHelpers
TestBlob = Struct.new(:path)
......@@ -1693,27 +1693,41 @@ describe Repository, models: true do
end
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
expect(repository.rugged.tags).to receive(:create)
.with('8.5', repository.commit('master').id,
hash_including(message: 'foo',
tagger: hash_including(name: user.name, email: user.email)))
.and_call_original
shared_examples 'adding tag' do
context 'with a valid target' do
it 'creates the tag' do
repository.add_tag(user, '8.5', 'master', 'foo')
repository.add_tag(user, '8.5', 'master', 'foo')
end
tag = repository.find_tag('8.5')
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
tag = repository.add_tag(user, '8.5', 'master', 'foo')
it 'returns a Gitlab::Git::Tag object' do
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
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,\
and tag SHA to post-receive hook' do
context 'when Gitaly operation_user_add_tag feature is disabled', skip_gitaly_mock: true 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)
update_hook = Gitlab::Git::Hook.new('update', project)
post_receive_hook = Gitlab::Git::Hook.new('post-receive', project)
......@@ -1738,12 +1752,6 @@ describe Repository, models: true do
.with(anything, anything, tag_sha, anything)
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
describe '#rm_branch' do
......@@ -1758,12 +1766,22 @@ describe Repository, models: true do
end
describe '#rm_tag' do
it 'removes a tag' do
expect(repository).to receive(:before_remove_tag)
shared_examples 'removing tag' do
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
......
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
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(:reported_abuse_reports).dependent(:destroy).class_name('AbuseReport') }
it { is_expected.to have_many(:custom_attributes).class_name('UserCustomAttribute') }
describe "#abuse_report" do
let(:current_user) { create(:user) }
......
......@@ -51,4 +51,18 @@ describe GlobalPolicy do
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
......@@ -164,18 +164,25 @@ describe API::Helpers do
end
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'
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
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
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
it "leaves user as is when sudo not specified" do
......@@ -198,24 +205,31 @@ describe API::Helpers do
allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false }
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'
expect(current_user).to be_nil
expect { current_user }.to raise_error /401/
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
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
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'])
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to be_nil
expect { current_user }.to raise_error /401/
end
it "leaves user as is when sudo not specified" do
......@@ -231,14 +245,14 @@ describe API::Helpers do
personal_access_token.revoke!
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to be_nil
expect { current_user }.to raise_error /401/
end
it 'does not allow expired tokens' do
personal_access_token.update_attributes!(expires_at: 1.day.ago)
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
......@@ -385,6 +399,18 @@ describe API::Helpers do
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
context 'with regular user' do
......@@ -524,11 +550,10 @@ describe API::Helpers do
context 'current_user is nil' do
before do
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
it 'returns a 401 response' do
expect { authenticate! }.to raise_error '401 - {"message"=>"401 Unauthorized"}'
expect { authenticate! }.to raise_error /401/
end
end
......@@ -536,35 +561,12 @@ describe API::Helpers do
let(:user) { build(:user) }
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(:initial_current_user).and_return(user)
expect_any_instance_of(self.class).to receive(:current_user).and_return(user)
end
it 'does not raise an error' do
expect { authenticate! }.not_to raise_error
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
......@@ -203,10 +203,9 @@ describe API::Users do
context "when authenticated and ldap is enabled" do
it "returns non-ldap user" do
User.delete_all
create :omniauth_user, provider: "ldapserver1"
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
username = user.username
expect(json_response.first["username"]).to eq username
......@@ -1939,4 +1938,8 @@ describe API::Users do
expect(impersonation_token.reload.revoked).to be_truthy
end
end
include_examples 'custom attributes endpoints', 'users' do
let(:attributable) { user }
end
end
......@@ -11,5 +11,16 @@ describe MergeRequests::PostMergeService do
describe '#execute' do
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
......@@ -28,7 +28,7 @@ describe Tags::CreateService do
it 'returns an error' do
expect(repository).to receive(:add_tag)
.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')
......
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