Commit 6625f479 authored by Regis's avatar Regis

Merge branch 'master' into auto-pipelines-vue

parents 55df5536 6c624821
......@@ -2,6 +2,26 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 8.15.3 (2017-01-06)
- Rename wiki_events to wiki_page_events in project hooks API to avoid errors. !0 (8425)
- Rename projects wth reserved names. !8234
- Cache project authorizations even when user has access to zero projects. !8327
- Fix a minor grammar error in merge request widget. !8337
- Fix unclear closing issue behaviour on Merge Request show page. !8345 (Gabriel Gizotti)
- fix border in login session tabs. !8346
- Copy, don't move uploaded avatar files. !8396
- Increases width of mini-pipeline-graph dropdown to prevent wrong position on chrome on ubuntu. !8399
- Removes invalid html and unneed CSS to prevent shaking in the pipelines tab. !8411
- Gitlab::LDAP::Person uses LDAP attributes configuration. !8418
- Fix 500 errors when creating a user with identity via API. !8442
- Whitelist next project names: assets, profile, public. !8470
- Fixed regression of note-headline-light where it was always placed on 2 lines, even on wide viewports.
- Fix 500 error when visit group from admin area if group name contains dot.
- Fix cross-project references copy to include the project reference.
- Fix 500 error renaming group.
- Fixed GFM dropdown not showing on new lines.
## 8.15.2 (2016-12-27)
- Fix finding the latest pipeline. !8301
......
......@@ -84,10 +84,14 @@ gem 'dropzonejs-rails', '~> 0.7.1'
# for backups
gem 'fog-aws', '~> 0.9'
gem 'fog-core', '~> 1.40'
gem 'fog-google', '~> 0.5'
gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1'
gem 'fog-rackspace', '~> 0.1.1'
# for Google storage
gem 'google-api-client', '~> 0.8.6'
# for aws storage
gem 'unf', '~> 0.1.4'
......
......@@ -58,6 +58,10 @@ GEM
attr_encrypted (3.0.3)
encryptor (~> 3.0.0)
attr_required (1.0.0)
autoparse (0.3.3)
addressable (>= 2.3.1)
extlib (>= 0.9.15)
multi_json (>= 1.0.0)
autoprefixer-rails (6.2.3)
execjs
json
......@@ -179,6 +183,7 @@ GEM
excon (0.52.0)
execjs (2.6.0)
expression_parser (0.9.0)
extlib (0.9.16)
factory_girl (4.7.0)
activesupport (>= 3.0.0)
factory_girl_rails (4.7.0)
......@@ -208,6 +213,10 @@ GEM
builder
excon (~> 0.49)
formatador (~> 0.2)
fog-google (0.5.0)
fog-core
fog-json
fog-xml
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
......@@ -279,6 +288,25 @@ GEM
json
multi_json
request_store (>= 1.0)
google-api-client (0.8.7)
activesupport (>= 3.2, < 5.0)
addressable (~> 2.3)
autoparse (~> 0.3)
extlib (~> 0.9)
faraday (~> 0.9)
googleauth (~> 0.3)
launchy (~> 2.4)
multi_json (~> 1.10)
retriable (~> 1.4)
signet (~> 0.6)
googleauth (0.5.1)
faraday (~> 0.9)
jwt (~> 1.4)
logging (~> 2.0)
memoist (~> 0.12)
multi_json (~> 1.11)
os (~> 0.9)
signet (~> 0.7)
grape (0.18.0)
activesupport
builder
......@@ -381,11 +409,16 @@ GEM
listen (3.0.5)
rb-fsevent (>= 0.9.3)
rb-inotify (>= 0.9)
little-plugger (1.1.4)
logging (2.1.0)
little-plugger (~> 1.1)
multi_json (~> 1.10)
loofah (2.0.3)
nokogiri (>= 1.5.9)
mail (2.6.4)
mime-types (>= 1.16, < 4)
mail_room (0.9.0)
memoist (0.15.0)
method_source (0.8.2)
mime-types (2.99.3)
mimemagic (0.3.0)
......@@ -473,6 +506,7 @@ GEM
org-ruby (0.9.12)
rubypants (~> 0.2)
orm_adapter (0.5.0)
os (0.9.6)
paranoia (2.2.0)
activerecord (>= 4.0, < 5.1)
parser (2.3.1.4)
......@@ -584,6 +618,7 @@ GEM
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
retriable (1.4.1)
rinku (2.0.0)
rotp (2.1.2)
rouge (2.0.7)
......@@ -675,6 +710,11 @@ GEM
sidekiq (>= 4.2.1)
sidekiq-limit_fetch (3.4.0)
sidekiq (>= 4)
signet (0.7.3)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (~> 1.5)
multi_json (~> 1.10)
simplecov (0.12.0)
docile (~> 1.1.0)
json (>= 1.8, < 3)
......@@ -841,6 +881,7 @@ DEPENDENCIES
flay (~> 2.6.1)
fog-aws (~> 0.9)
fog-core (~> 1.40)
fog-google (~> 0.5)
fog-local (~> 0.3)
fog-openstack (~> 0.1)
fog-rackspace (~> 0.1.1)
......@@ -856,6 +897,7 @@ DEPENDENCIES
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2)
gon (~> 6.1.0)
google-api-client (~> 0.8.6)
grape (~> 0.18.0)
grape-entity (~> 0.6.0)
haml_lint (~> 0.18.2)
......
......@@ -45,14 +45,28 @@
const issue = this.list.findIssue(this.detailIssue.issue.id);
if (issue) {
const offsetLeft = this.$el.offsetLeft;
const boardsList = document.querySelectorAll('.boards-list')[0];
const right = (this.$el.offsetLeft + this.$el.offsetWidth) - boardsList.offsetWidth;
const left = boardsList.scrollLeft - this.$el.offsetLeft;
const left = boardsList.scrollLeft - offsetLeft;
let right = (offsetLeft + this.$el.offsetWidth);
if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) {
// -290 here because width of boardsList is animating so therefore
// getting the width here is incorrect
// 290 is the width of the sidebar
right -= (boardsList.offsetWidth - 290);
} else {
right -= boardsList.offsetWidth;
}
if (right - boardsList.scrollLeft > 0) {
boardsList.scrollLeft = right;
$(boardsList).animate({
scrollLeft: right
}, this.sortableOptions.animation);
} else if (left > 0) {
boardsList.scrollLeft = this.$el.offsetLeft;
$(boardsList).animate({
scrollLeft: offsetLeft
}, this.sortableOptions.animation);
}
}
},
......@@ -65,7 +79,7 @@
}
},
mounted () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled,
group: 'boards',
draggable: '.is-draggable',
......@@ -84,7 +98,7 @@
}
});
this.sortable = Sortable.create(this.$el.parentNode, options);
this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
},
});
})();
......@@ -20,6 +20,7 @@
gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
let defaultSortOptions = {
animation: 200,
forceFallback: true,
fallbackClass: 'is-dragging',
fallbackOnBody: true,
......
......@@ -210,7 +210,9 @@
new gl.Members();
new UsersSelect();
break;
case 'projects:project_members:index':
case 'projects:members:show':
new gl.MemberExpirationDate('.js-access-expiration-date-groups');
new GroupsSelect();
new gl.MemberExpirationDate();
new gl.Members();
new UsersSelect();
......@@ -256,10 +258,6 @@
case 'projects:artifacts:browse':
new BuildArtifacts();
break;
case 'projects:group_links:index':
new gl.MemberExpirationDate();
new GroupsSelect();
break;
case 'search:show':
new Search();
break;
......
......@@ -80,9 +80,12 @@
}
parseSelectedDate() {
this.rawSelectedDate = $("input[name='" + this.fieldName + "']").val();
this.rawSelectedDate = $(`input[name='${this.fieldName}']`).val();
if (this.rawSelectedDate.length) {
let dateObj = new Date(this.rawSelectedDate);
// Construct Date object manually to avoid buggy dateString support within Date constructor
const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10));
const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]);
this.displayedDate = $.datepicker.formatDate('M d, yy', dateObj);
} else {
this.displayedDate = 'No due date';
......
......@@ -216,7 +216,7 @@
<th class="environments-deploy">Last deployment</th>
<th class="environments-build">Build</th>
<th class="environments-commit">Commit</th>
<th class="environments-date"></th>
<th class="environments-date">Created</th>
<th class="hidden-xs environments-actions"></th>
</tr>
</thead>
......
......@@ -44,9 +44,25 @@
}
};
gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
var insertText, inserted, selectedSplit, startChar;
var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine;
removedLastNewLine = false;
removedFirstNewLine = false;
// Remove the first newline
if (selected.indexOf('\n') === 0) {
removedFirstNewLine = true;
selected = selected.replace(/\n+/, '');
}
// Remove the last newline
if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
removedLastNewLine = true;
selected = selected.replace(/\n$/, '');
}
selectedSplit = selected.split('\n');
startChar = !wrap && textArea.selectionStart > 0 ? '\n' : '';
if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) {
if (blockTag != null) {
insertText = this.blockTagText(text, textArea, blockTag, selected);
......@@ -62,6 +78,15 @@
} else {
insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
}
if (removedFirstNewLine) {
insertText = '\n' + insertText;
}
if (removedLastNewLine) {
insertText += '\n';
}
if (document.queryCommandSupported('insertText')) {
inserted = document.execCommand('insertText', false, insertText);
}
......@@ -74,9 +99,9 @@
document.execCommand("ms-endUndoUnit");
} catch (error) {}
}
return this.moveCursor(textArea, tag, wrap);
return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
};
gl.text.moveCursor = function(textArea, tag, wrapped) {
gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
var pos;
if (!textArea.setSelectionRange) {
return;
......@@ -87,6 +112,11 @@
} else {
pos = textArea.selectionStart;
}
if (removedLastNewLine) {
pos -= 1;
}
return textArea.setSelectionRange(pos, pos);
}
};
......
/* eslint-disable func-names, space-before-function-paren, vars-on-top, no-var, object-shorthand, comma-dangle, max-len */
(function() {
(() => {
// Add datepickers to all `js-access-expiration-date` elements. If those elements are
// children of an element with the `clearable-input` class, and have a sibling
// `js-clear-input` element, then show that element when there is a value in the
// datepicker, and make clicking on that element clear the field.
//
gl.MemberExpirationDate = function() {
window.gl = window.gl || {};
gl.MemberExpirationDate = (selector = '.js-access-expiration-date') => {
function toggleClearInput() {
$(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
}
var inputs = $('.js-access-expiration-date');
const inputs = $(selector);
inputs.datepicker({
dateFormat: 'yy-mm-dd',
minDate: 1,
onSelect: function () {
onSelect: function onSelect() {
$(this).trigger('change');
toggleClearInput.call(this);
}
},
});
inputs.next('.js-clear-input').on('click', function(event) {
inputs.next('.js-clear-input').on('click', function clicked(event) {
event.preventDefault();
var input = $(this).closest('.clearable-input').find('.js-access-expiration-date');
const input = $(this).closest('.clearable-input').find(selector);
input.datepicker('setDate', null)
.trigger('change');
toggleClearInput.call(input);
......
......@@ -895,7 +895,9 @@
new GLForm($editForm.find('form'));
$editForm.find('form').attr('action', postUrl);
$editForm.find('form')
.attr('action', postUrl)
.attr('data-remote', 'true');
$editForm.find('.js-form-target-id').val(targetId);
$editForm.find('.js-form-target-type').val(targetType);
$editForm.find('.js-note-text').focus().val(originalContent);
......
......@@ -23,7 +23,7 @@
});
$('.no-template', this.dropdown.parent()).on('click', () => {
this.currentTemplate = '';
this.currentTemplate.content = '';
this.setInputValueToTemplateContent();
$('.dropdown-toggle-text', this.dropdown).text('Choose a template');
});
......
......@@ -163,6 +163,10 @@ ul.content-list {
&:last-child {
margin-right: 0;
@media(max-width: $screen-xs-max) {
margin: 0 auto;
}
}
}
......
......@@ -74,6 +74,7 @@
height: 475px; // Needed for PhantomJS
height: calc(100vh - 220px);
min-height: 475px;
transition: width .2s;
&.is-compact {
width: calc(100% - 290px);
......@@ -338,3 +339,18 @@
}
}
}
.right-sidebar.right-sidebar-expanded {
&.boards-sidebar-slide-enter-active,
&.boards-sidebar-slide-leave-active {
transition: width .2s,
padding .2s;
}
&.boards-sidebar-slide-enter,
&.boards-sidebar-slide-leave-active {
width: 0;
padding-left: 0;
padding-right: 0;
}
}
......@@ -25,7 +25,7 @@
}
.form-horizontal {
margin-top: 5px;
margin-top: 20px;
@media (min-width: $screen-sm-min) {
display: -webkit-flex;
......@@ -98,6 +98,10 @@
padding-right: 35px;
@media (min-width: $screen-sm-min) {
width: 250px;
}
@media (min-width: $screen-md-min) {
width: 350px;
}
......
module GlobalMilestones
extend ActiveSupport::Concern
def milestones
epoch = DateTime.parse('1970-01-01')
@milestones = MilestonesFinder.new.execute(@projects, params)
@milestones = GlobalMilestone.build_collection(@milestones)
@milestones = @milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
end
def milestone
milestones = Milestone.of_projects(@projects).where(title: params[:title])
if milestones.present?
@milestone = GlobalMilestone.new(params[:title], milestones)
else
render_404
end
end
end
class Dashboard::MilestonesController < Dashboard::ApplicationController
include GlobalMilestones
before_action :projects
before_action :milestone, only: [:show]
......@@ -17,4 +15,15 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
def show
end
private
def milestones
@milestones = GlobalMilestone.build_collection(@projects, params)
end
def milestone
@milestone = GlobalMilestone.build(@projects, params[:title])
render_404 unless @milestone
end
end
class Groups::MilestonesController < Groups::ApplicationController
include GlobalMilestones
before_action :group_projects
before_action :milestone, only: [:show, :update]
before_action :authorize_admin_milestones!, only: [:new, :create, :update]
......@@ -73,4 +71,13 @@ class Groups::MilestonesController < Groups::ApplicationController
def milestone_path(title)
group_milestone_path(@group, title.to_slug.to_s, title: title)
end
def milestones
@milestones = GroupMilestone.build_collection(@group, @projects, params)
end
def milestone
@milestone = GroupMilestone.build(@group, @projects, params[:title])
render_404 unless @milestone
end
end
......@@ -4,10 +4,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
before_action :authorize_admin_project_member!, only: [:update]
def index
@group_links = project.project_group_links.all
@skip_groups = @group_links.pluck(:group_id)
@skip_groups << project.namespace_id unless project.personal?
redirect_to namespace_project_settings_members_path
end
def create
......@@ -25,7 +22,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
flash[:alert] = 'Please select a group.'
end
redirect_to namespace_project_group_links_path(project.namespace, project)
redirect_to namespace_project_settings_members_path(project.namespace, project)
end
def update
......@@ -39,7 +36,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
respond_to do |format|
format.html do
redirect_to namespace_project_group_links_path(project.namespace, project)
redirect_to namespace_project_settings_members_path(project.namespace, project)
end
format.js { head :ok }
end
......
......@@ -6,54 +6,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
def index
@sort = params[:sort].presence || sort_value_name
@group_links = @project.project_group_links
@project_members = @project.project_members
@project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
group = @project.group
if group
# We need `.where.not(user_id: nil)` here otherwise when a group has an
# invitee, it would make the following query return 0 rows since a NULL
# user_id would be present in the subquery
# See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
# FIXME: This whole logic should be moved to a finder!
non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id)
group_members = group.group_members.where.not(user_id: non_null_user_ids)
group_members = group_members.non_invite unless can?(current_user, :admin_group, @group)
end
if params[:search].present?
user_ids = @project.users.search(params[:search]).select(:id)
@project_members = @project_members.where(user_id: user_ids)
if group_members
user_ids = group.users.search(params[:search]).select(:id)
group_members = group_members.where(user_id: user_ids)
end
@group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end
wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"]
wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members
@project_members = Member.
where(wheres.join(' OR ')).
sort(@sort).
page(params[:page])
@requesters = AccessRequestsFinder.new(@project).execute(current_user)
@project_member = @project.project_members.new
sort = params[:sort].presence || sort_value_name
redirect_to namespace_project_settings_members_path(@project.namespace, @project, sort: sort)
end
def create
status = Members::CreateService.new(@project, current_user, params).execute
redirect_url = namespace_project_project_members_path(@project.namespace, @project)
redirect_url = namespace_project_settings_members_path(@project.namespace, @project)
if status
redirect_to redirect_url, notice: 'Users were successfully added.'
......@@ -76,14 +36,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController
respond_to do |format|
format.html do
redirect_to namespace_project_project_members_path(@project.namespace, @project)
redirect_to namespace_project_settings_members_path(@project.namespace, @project)
end
format.js { head :ok }
end
end
def resend_invite
redirect_path = namespace_project_project_members_path(@project.namespace, @project)
redirect_path = namespace_project_settings_members_path(@project.namespace, @project)
@project_member = @project.project_members.find(params[:id])
......@@ -106,7 +66,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
return render_404
end
redirect_to(namespace_project_project_members_path(project.namespace, project),
redirect_to(namespace_project_settings_members_path(project.namespace, project),
notice: notice)
end
......
module Projects
module Settings
class MembersController < Projects::ApplicationController
include SortingHelper
def show
@sort = params[:sort].presence || sort_value_name
@group_links = @project.project_group_links
@project_members = @project.project_members
@project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
group = @project.group
# group links
@group_links = @project.project_group_links.all
@skip_groups = @group_links.pluck(:group_id)
@skip_groups << @project.namespace_id unless @project.personal?
if group
# We need `.where.not(user_id: nil)` here otherwise when a group has an
# invitee, it would make the following query return 0 rows since a NULL
# user_id would be present in the subquery
# See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values
group_members = MembersFinder.new(@project_members, group).execute(current_user)
end
if params[:search].present?
user_ids = @project.users.search(params[:search]).select(:id)
@project_members = @project_members.where(user_id: user_ids)
if group_members
user_ids = group.users.search(params[:search]).select(:id)
group_members = group_members.where(user_id: user_ids)
end
@group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
end
wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"]
wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members
@project_members = Member.
where(wheres.join(' OR ')).
sort(@sort).
page(params[:page])
@requesters = AccessRequestsFinder.new(@project).execute(current_user)
@project_member = @project.project_members.new
end
end
end
end
class MembersFinder < Projects::ApplicationController
def initialize(project_members, project_group)
@project_members = project_members
@project_group = project_group
end
def execute(current_user)
non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id)
group_members = @project_group.group_members.where.not(user_id: non_null_user_ids)
group_members = group_members.non_invite unless can?(current_user, :admin_group, @project_group)
group_members
end
end
......@@ -206,4 +206,9 @@ module GitlabRoutingHelper
file_namespace_project_build_artifacts_path(*args)
end
end
# Settings
def project_settings_members_path(project, *args)
namespace_project_settings_members_path(project.namespace, project, *args)
end
end
......@@ -39,7 +39,7 @@ module SearchHelper
# Autocomplete results for various settings pages
def default_autocomplete
[
{ category: "Settings", label: "Profile settings", url: profile_path },
{ category: "Settings", label: "User settings", url: profile_path },
{ category: "Settings", label: "SSH Keys", url: profile_keys_path },
{ category: "Settings", label: "Dashboard", url: root_path },
{ category: "Settings", label: "Admin Section", url: admin_root_path },
......@@ -75,7 +75,7 @@ module SearchHelper
{ category: "Current Project", label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) },
{ category: "Current Project", label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) },
{ category: "Current Project", label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) },
{ category: "Current Project", label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) },
{ category: "Current Project", label: "Members", url: namespace_project_settings_members_path(@project.namespace, @project) },
{ category: "Current Project", label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) },
]
else
......
......@@ -36,8 +36,8 @@ module Milestoneish
def issues_visible_to_user(user)
memoize_per_user(user, :issues_visible_to_user) do
params = try(:project_id) ? { project_id: project_id } : {}
IssuesFinder.new(user, params).execute.where(milestone_id: milestoneish_ids)
IssuesFinder.new(user, issues_finder_params)
.execute.where(milestone_id: milestoneish_ids)
end
end
......@@ -72,4 +72,10 @@ module Milestoneish
@memoized[method_name] ||= {}
@memoized[method_name][user.try!(:id)] ||= yield
end
# override in a class that includes this module to get a faster query
# from IssuesFinder
def issues_finder_params
{}
end
end
class GlobalMilestone
include Milestoneish
EPOCH = DateTime.parse('1970-01-01')
attr_accessor :title, :milestones
alias_attribute :name, :title
......@@ -8,13 +10,22 @@ class GlobalMilestone
@first_milestone
end
def self.build_collection(milestones)
milestones = milestones.group_by(&:title)
def self.build_collection(projects, params)
child_milestones = MilestonesFinder.new.execute(projects, params)
milestones.map do |title, milestones|
milestones_relation = Milestone.where(id: milestones.map(&:id))
milestones = child_milestones.select(:id, :title).group_by(&:title).map do |title, grouped|
milestones_relation = Milestone.where(id: grouped.map(&:id))
new(title, milestones_relation)
end
milestones.sort_by { |milestone| milestone.due_date || EPOCH }
end
def self.build(projects, title)
child_milestones = Milestone.of_projects(projects).where(title: title)
return if child_milestones.blank?
new(title, child_milestones)
end
def initialize(title, milestones)
......
class GroupMilestone < GlobalMilestone
attr_accessor :group
def self.build_collection(group, projects, params)
super(projects, params).each do |milestone|
milestone.group = group
end
end
def self.build(group, projects, title)
super(projects, title).tap do |milestone|
milestone.group = group if milestone
end
end
def issues_finder_params
{ group_id: group.id }
end
end
......@@ -49,6 +49,10 @@ class Key < ActiveRecord::Base
"key-#{id}"
end
def update_last_used_at
UseKeyWorker.perform_async(self.id)
end
def add_to_shell
GitlabShellWorker.perform_async(
:add_key,
......
......@@ -221,7 +221,7 @@ class MergeRequest < ActiveRecord::Base
# true base commit, so we can't simply have `#diff_base_commit` fall back on
# this method.
def likely_diff_base_commit
first_commit.parent || first_commit
first_commit.try(:parent) || first_commit
end
def diff_start_commit
......
......@@ -202,4 +202,8 @@ class Milestone < ActiveRecord::Base
errors.add(:start_date, "Can't be greater than due date")
end
end
def issues_finder_params
{ project_id: project_id }
end
end
......@@ -37,6 +37,10 @@ class NotificationSetting < ActiveRecord::Base
:success_pipeline
]
EXCLUDED_WATCHER_EVENTS = [
:success_pipeline
]
store :events, accessors: EMAIL_EVENTS, coder: JSON
before_create :set_events
......
......@@ -591,7 +591,10 @@ class NotificationService
custom_action = build_custom_key(action, target)
recipients = target.participants(current_user)
unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
recipients = add_project_watchers(recipients, project)
end
recipients = add_custom_notifications(recipients, project, custom_action)
recipients = reject_mention_users(recipients, project)
......
......@@ -36,7 +36,7 @@ module Projects
def groups
current_user.authorized_groups.sort_by(&:path).map do |group|
count = group.users.count
{ username: group.path, name: group.name, count: count, avatar_url: group.avatar.url }
{ username: group.path, name: group.name, count: count, avatar_url: group.avatar_url }
end
end
......
......@@ -15,7 +15,7 @@ class ProjectPathValidator < ActiveModel::EachValidator
# 'tree' as project name and 'deploy_keys' as route.
#
RESERVED = (NamespaceValidator::RESERVED -
%w[dashboard help ci admin search notes services] +
%w[dashboard help ci admin search notes services assets profile public] +
%w[tree commits wikis new edit create update logs_tree
preview blob blame raw files create_dir find_file]).freeze
......
......@@ -44,7 +44,7 @@
%li
= link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username }
%li
= link_to "Profile Settings", profile_path, aria: { label: "Profile Settings" }
= link_to "Settings", profile_path, aria: { label: "Settings" }
%li
= link_to "Help", help_path, aria: { label: "Help" }
%li.divider
......
- if project_nav_tab? :team
= nav_link(controller: [:project_members, :teams]) do
= link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
= nav_link(controller: [:members, :teams]) do
= link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
%span
Members
- if can_edit
- if @project.allowed_to_share_with_group?
= nav_link(controller: :group_links) do
= link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do
%span
Groups
= nav_link(controller: :deploy_keys) do
= link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
%span
......
- page_title "Profile Settings"
- header_title "Profile Settings", profile_path unless header_title
- page_title "User Settings"
- header_title "User Settings", profile_path unless header_title
- sidebar "dashboard"
- nav "profile"
......
......@@ -6,6 +6,9 @@
= key.title
.description
= key.fingerprint
.last-used-at
last used:
= key.last_used_at ? time_ago_with_tooltip(key.last_used_at) : 'n/a'
.pull-right
%span.key-created-at
created #{time_ago_with_tooltip(key.created_at)}
......
......@@ -11,6 +11,9 @@
%li
%span.light Created on:
%strong= @key.created_at.to_s(:medium)
%li
%span.light Last used on:
%strong= @key.last_used_at.try(:to_s, :medium) || 'N/A'
.col-md-8
%p
......
%board-sidebar{ "inline-template" => true,
":current-user" => "#{current_user ? current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) : {}}" }
%transition{ name: "boards-sidebar-slide" }
%aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" }
.issuable-sidebar
.block.issuable-sidebar-header
......
......@@ -20,10 +20,10 @@
.form-group
= label_tag :expires_at, 'Access expiration date', class: 'label-light'
.clearable-input
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date-groups', placeholder: 'Select access expiration date', id: 'expires_at_groups'
%i.clear-icon.js-clear-input
.help-block
On this date, all users in the group will automatically lose access to this project.
On this date, all members in the group will automatically lose access to this project.
= submit_tag "Share", class: "btn btn-create"
.col-lg-9.col-lg-offset-3
%hr
......
......@@ -50,7 +50,6 @@
.content-block.content-block-small
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true
- if @commits_count.nonzero?
.merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
%div{ class: container_class }
%ul.merge-request-tabs.nav-links.no-top.no-bottom
......
%ol#commits-list.list-unstyled
- if @commits.empty?
.commits-empty
%h4
There are no commits yet.
= custom_icon ('illustration_no_commits')
- else
%ol#commits-list.list-unstyled
= render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch
.note-edit-form
= form_tag '#', method: :put, remote: true, class: 'edit-note common-note-form js-quick-submit' do
= hidden_field_tag :authenticity_token, form_authenticity_token
= form_tag '#', method: :put, class: 'edit-note common-note-form js-quick-submit' do
= hidden_field_tag :target_id, '', class: 'js-form-target-id'
= hidden_field_tag :target_type, '', class: 'js-form-target-type'
= render layout: 'projects/md_preview', locals: { preview_class: 'md-preview', referenced_users: true } do
......
......@@ -23,4 +23,4 @@
to post a comment
:javascript
var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")
var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, project_id: @project, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")
.row.prepend-top-default
.col-lg-3.settings-sidebar
%h4.prepend-top-0
Members
- if can?(current_user, :admin_project_member, @project)
%p
Add a new member to
%strong= @project.name
.col-lg-9
.light.prepend-top-default
- if can?(current_user, :admin_project_member, @project)
= render "projects/project_members/new_project_member"
= render 'shared/members/requests', membership_source: @project, requesters: @requesters
.append-bottom-default.clearfix
%h5.member.existing-title
Existing members and groups
- if @group_links.any?
= render 'projects/project_members/groups', group_links: @group_links
= render 'projects/project_members/team', members: @project_members
= paginate @project_members, theme: "gitlab"
= form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'users-project-form' } do |f|
.row
.col-md-4.col-lg-6
= users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true)
.form-group
= users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true, placeholder: "Search for members to update or invite")
.help-block.append-bottom-10
Search for users by name, username, or email, or invite new ones using their email address.
.col-md-3.col-lg-2
Search for members by name, username, or email, or invite new ones using their email address.
.form-group
= select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select"
.help-block.append-bottom-10
= link_to "Read more", help_page_path("user/permissions"), class: "vlink"
about role permissions
.col-md-3.col-lg-2
.form-group
.clearable-input
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
%i.clear-icon.js-clear-input
.help-block.append-bottom-10
On this date, the user(s) will automatically lose access to this project.
.col-md-2
= f.submit "Add to project", class: "btn btn-create btn-block"
On this date, the member(s) will automatically lose access to this project.
= f.submit "Add to project", class: "btn btn-create"
= link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default", title: "Import members from another project"
.panel.panel-default
.panel-heading
Users with access to
Members with access to
%strong #{@project.name}
%span.badge= @project_members.total_count
= form_tag namespace_project_settings_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
.form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
%button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
= icon("search")
= render 'shared/members/sort_dropdown'
%ul.content-list
= render partial: 'shared/members/member', collection: members, as: :member
......@@ -12,5 +12,4 @@
.form-actions
= button_tag 'Import project members', class: "btn btn-create"
= link_to "Cancel", namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-cancel"
= link_to "Cancel", namespace_project_settings_members_path(@project.namespace, @project), class: "btn btn-cancel"
- page_title "Members"
.project-members-page.prepend-top-default
%h4.project-members-title.clearfix
Members
= link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default pull-right hidden-xs", title: "Import members from another project"
- if can?(current_user, :admin_project_member, @project)
.project-members-new.append-bottom-default
%p.clearfix
Add new user to
%strong= @project.name
= render "new_project_member"
= render 'shared/members/requests', membership_source: @project, requesters: @requesters
.append-bottom-default.clearfix
%h5.member.existing-title
Existing users and groups
= form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
.form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
%button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
= icon("search")
= render 'shared/members/sort_dropdown'
- if @group_links.any?
= render 'groups', group_links: @group_links
= render 'team', members: @project_members
= paginate @project_members, theme: "gitlab"
- page_title "Members"
= render "projects/project_members/index"
- if can?(current_user, :admin_project, @project)
- if @project.allowed_to_share_with_group?
= render "projects/group_links/index"
......@@ -97,7 +97,7 @@
remove due date
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.selectbox.hide-collapsed
= f.hidden_field :due_date, value: issuable.due_date
= f.hidden_field :due_date, value: issuable.due_date.try(:strftime, 'yy-mm-dd')
.dropdown
%button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable) } }
%span.dropdown-toggle-text Due date
......
......@@ -37,7 +37,6 @@
%i.clear-icon.js-clear-input
- if can_admin_member
= link_to namespace_project_group_link_path(@project.namespace, @project, group_link),
remote: true,
method: :delete,
data: { confirm: "Are you sure you want to remove #{group.name}?" },
class: 'btn btn-remove prepend-left-10' do
......
class UseKeyWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
def perform(key_id)
key = Key.find(key_id)
key.touch(:last_used_at)
rescue ActiveRecord::RecordNotFound
Rails.logger.error("UseKeyWorker: couldn't find key with ID=#{key_id}, skipping job")
false
end
end
---
title: Fix double spaced CI log
merge_request: 8349
author: Jared Deckard <jared.deckard@gmail.com>
---
title: fix border in login session tabs
merge_request: 8346
author:
---
title: Adds label to Environments "Date Created"
merge_request: 8376
author: Saad Shahd
---
title: Combined the settings options project members and groups into a single one
called members
merge_request:
author:
---
title: Re-order update steps in the 8.14 -> 8.15 upgrade guide
merge_request:
author:
---
title: Don't instrument 405 Grape calls
merge_request: 8445
author:
---
title: Cache project authorizations even when user has access to zero projects
merge_request: 8327
author:
---
title: display merge request discussion tab for empty branches
merge_request: 8347
author:
---
title: Increases width of mini-pipeline-graph dropdown to prevent wrong position on chrome on ubuntu
merge_request: 8399
author:
---
title: Precompile all JavaScript fixtures
merge_request: 8384
author:
---
title: Removes invalid html and unneed CSS to prevent shaking in the pipelines tab
merge_request: 8411
author:
---
title: 26352 Change Profile settings to User / Settings
merge_request:
author:
---
title: Rename wiki_events to wiki_page_events in project hooks API to avoid errors
merge_request: Robert Schilling
author: 8425
---
title: Fix 500 error when visit group from admin area if group name contains dot
merge_request:
author:
---
title: Rename projects wth reserved names
merge_request: 8234
author:
---
title: Log LDAP blocking/unblocking events to application log
merge_request: 8042
author: Markus Koller
---
title: Fix broken url on group avatar
merge_request: 8464
author: hogewest
---
title: Fix cross-project references copy to include the project reference
merge_request:
author:
---
title: Fix 500 error renaming group
merge_request:
author:
---
title: Fix a minor grammar error in merge request widget
merge_request: 8337
author:
---
title: Fix date inconsistency on due date picker
merge_request: 7422
author: Giuliano Varriale
---
title: Record and show last used date of SSH Keys
merge_request: 8113
author: Vincent Wong
---
title: Fixed GFM dropdown not showing on new lines
title: Added animations to issue boards interactions
merge_request:
author:
---
title: Gitlab::LDAP::Person uses LDAP attributes configuration
merge_request: 8418
author:
---
title: Fix unclear closing issue behaviour on Merge Request show page
merge_request: 8345
author: Gabriel Gizotti
---
title: Copy, don't move uploaded avatar files
merge_request: 8396
author:
---
title: Fixed regression of note-headline-light where it was always placed on 2 lines, even on wide viewports
merge_request:
author:
---
title: Make successful pipeline emails off for watchers
merge_request: 8176
author:
---
title: Speed up group milestone index by passing group_id to IssuesFinder
merge_request: 8363
author:
---
title: Re-add Google Cloud Storage as a backup strategy
merge_request:
author:
......@@ -307,6 +307,10 @@ constraints(ProjectUrlConstrainer.new) do
end
end
namespace :settings do
resource :members, only: [:show]
end
# Since both wiki and repository routing contains wildcard characters
# its preferable to keep it below all other project routes
draw :wiki
......
......@@ -29,6 +29,7 @@
- [email_receiver, 2]
- [emails_on_push, 2]
- [mailers, 2]
- [use_key, 1]
- [repository_fork, 1]
- [repository_import, 1]
- [project_service, 1]
......
class AddLastUsedAtToKey < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :keys, :last_used_at, :datetime
end
end
......@@ -10,7 +10,6 @@ class RenameReservedProjectNames < ActiveRecord::Migration
KNOWN_PATHS = %w(.well-known
all
assets
blame
blob
commits
......@@ -18,7 +17,6 @@ class RenameReservedProjectNames < ActiveRecord::Migration
create_dir
edit
files
files
find_file
groups
hooks
......@@ -26,11 +24,8 @@ class RenameReservedProjectNames < ActiveRecord::Migration
logs_tree
merge_requests
new
new
preview
profile
projects
public
raw
repository
robots.txt
......
......@@ -528,6 +528,7 @@ ActiveRecord::Schema.define(version: 20161227192806) do
t.string "fingerprint"
t.boolean "public", default: false, null: false
t.boolean "can_push", default: false, null: false
t.datetime "last_used_at"
end
add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree
......
......@@ -298,8 +298,11 @@ LDAP server please double-check the LDAP `port` and `method` settings used by
GitLab. Common combinations are `method: 'plain'` and `port: 389`, OR
`method: 'ssl'` and `port: 636`.
### Login with valid credentials rejected
### Troubleshooting
If there is an unexpected error while authenticating the user with the LDAP
backend, the login is rejected and details about the error are logged to
If a user account is blocked or unblocked due to the LDAP configuration, a
message will be logged to `application.log`.
If there is an unexpected error during an LDAP lookup (configuration error,
timeout), the login is rejected and a message will be logged to
`production.log`.
......@@ -71,8 +71,8 @@ If you want to use Gmail / Google Apps with Reply by email, make sure you have
[IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018)
and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255).
To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
[these instructions](./postfix.md).
To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
[Postfix setup documentation](reply_by_email_postfix_setup.md).
### Omnibus package installations
......
......@@ -4,12 +4,14 @@ The copy for GitLab is clear and direct. We strike a clear balance between profe
The copy and messaging is a core part of the experience of GitLab and the conversation with our users. Follow the below conventions throughout GitLab.
Portions of this page are inspired by work found in the [Material Design guidelines][material design].
>**Note:**
We are currently inconsistent with this guidance. Images below are created to illustrate the point. As this guidance is refined, we will ensure that our experiences align.
## Contents
* [Brevity](#brevity)
* [Sentence case](#sentence-case)
* [Capitalization and punctuation](#capitalization-and-punctuation)
* [Terminology](#terminology)
---
......@@ -29,8 +31,71 @@ Preferrably use context and placement of controls to make it obvious what clicki
---
## Sentence case
Use sentence case for all titles, headings, labels, menu items, and buttons.
## Capitalization and punctuation
### Case
Use sentence case for titles, headings, labels, menu items, and buttons. Use title case when referring to [features][features] or [products][products]. Note that some features are also objects (e.g. “Merge Requests” and “merge requests”).
| :white_check_mark: Do | :no_entry_sign: Don’t |
| --- | --- |
| Add issues to the Issue Board feature in GitLab Hosted | Add Issues To The Issue Board Feature In GitLab Hosted |
### Avoid periods
Avoid using periods in solitary sentences in these elements:
* Labels
* Hover text
* Bulleted lists
* Dialog body text
Periods should be used for:
* Lists or dialogs with multiple sentences
* Any sentence followed by a link
| :white_check_mark: **Do** place periods after sentences followed by a link | :no_entry_sign: **Don’t** place periods after a link if it‘s not followed by a sentence |
| --- | --- |
| Mention someone to notify them. [Learn more](#) | Mention someone to notify them. [Learn more](#). |
| :white_check_mark: **Do** skip periods after solo sentences of body text | :no_entry_sign: **Don’t** place periods after body text if there is only a single sentence |
| --- | --- |
| To see the available commands, enter `/gitlab help` | To see the available commands, enter `/gitlab help`. |
### Use contractions
Don’t make a sentence harder to understand just to follow this rule. For example, “do not” can give more emphasis than “don’t” when needed.
| :white_check_mark: Do | :no_entry_sign: Don’t |
| --- | --- |
| it’s, can’t, wouldn’t, you’re, you’ve, haven’t, don’t | it is, cannot, would not, it’ll, should’ve |
### Use numerals for numbers
Use “1, 2, 3” instead of “one, two, three” for numbers. One exception is when mixing uses of numbers, such as “Enter two 3s.”
| :white_check_mark: Do | :no_entry_sign: Don’t |
| --- | --- |
| 3 new commits | Three new commits |
### Punctuation
Omit punctuation after phrases and labels to create a cleaner and more readable interface. Use punctuation to add clarity or be grammatically correct.
| Punctuation mark | Copy and paste | HTML entity | Unicode | Mac shortcut | Windows shortcut | Description |
|---|---|---|---|---|---|---|
| Period | **.** | | | | | Omit for single sentences in affordances like labels, hover text, bulleted lists, and dialog body text.<br><br>Use in lists or dialogs with multiple sentences, and any sentence followed by a link or inline code.<br><br>Place inside quotation marks unless you’re telling the reader what to enter and it’s ambiguous whether to include the period. |
| Comma | **,** | | | | | Place inside quotation marks.<br><br>Use a [serial comma][serial comma] in lists of three or more terms. |
| Exclamation point | **!** | | | | | Avoid exclamation points as they tend to come across as shouting. Some exceptions include greetings or congratulatory messages. |
| Colon | **:** | `&#58;` | `\u003A` | | | Omit from labels, for example, in the labels for fields in a form. |
| Apostrophe | **’** | `&rsquo;` | `\u2019` | <kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>]</kbd> | <kbd>Alt</kbd>+<kbd>0 1 4 6</kbd> | Use for contractions (I’m, you’re, ’89) and to show possession.<br><br>To show possession, add an *’s* to all single nouns and names, even if they already end in an *s*: “Your issues’s status was changed.” For singular proper names ending in *s*, use only an apostrophe: “James’ commits.” For plurals of a single letter, add an *’s*: “Dot your i’s and cross your t’s.”<br><br>Omit for decades or acronyms: “the 1990s”, “MRs.” |
| Quotation marks | **“**<br><br>**”**<br><br>**‘**<br><br>**’** | `&ldquo;`<br><br>`&rdquo;`<br><br>`&lsquo;`<br><br>`&rsquo;` | `\u201C`<br><br>`\u201D`<br><br>`\u2018`<br><br>`\u2019` | <kbd>⌥ Option</kbd>+<kbd>[</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>[</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>]</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>]</kbd> | <kbd>Alt</kbd>+<kbd>0 1 4 7</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 8</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 5</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 6</kbd> | Use proper quotation marks (also known as smart quotes, curly quotes, or typographer’s quotes) for quotes. Single quotation marks are used for quotes inside of quotes.<br><br>The right single quotation mark symbol is also used for apostrophes.<br><br>Don’t use primes, straight quotes, or free-standing accents for quotation marks. |
| Primes | **′**<br><br>**″** | `&prime;`<br><br>`&Prime;` | `\u2032`<br><br>`\u2033` | | <kbd>Alt</kbd>+<kbd>8 2 4 2</kbd><br><br><kbd>Alt</kbd>+<kbd>8 2 4 3</kbd> | Use prime (′) only in abbreviations for feet, arcminutes, and minutes: 3° 15′<br><br>Use double-prime (″) only in abbreviations for inches, arcseconds, and seconds: 3° 15′ 35″<br><br>Don’t use quotation marks, straight quotes, or free-standing accents for primes. |
| Straight quotes and accents | **"**<br><br>**'**<br><br>**`**<br><br>**´** | `&quot;`<br><br>`&#39;`<br><br>`&#96;`<br><br>`&acute;` | `\u0022`<br><br>`\u0027`<br><br>`\u0060`<br><br>`\u00B4` | | | Don’t use straight quotes or free-standing accents for primes or quotation marks.<br><br>Proper typography never uses straight quotes. They are left over from the age of typewriters and their only modern use is for code. |
| Ellipsis | **…** | `&hellip;` | | <kbd>⌥ Option</kbd>+<kbd>;</kbd> | <kbd>Alt</kbd>+<kbd>0 1 3 3</kbd> | Use to indicate an action in progress (“Downloading…”) or incomplete or truncated text. No space before the ellipsis.<br><br>Omit from menu items or buttons that open a dialog or start some other process. |
| Chevrons | **«**<br><br>**»**<br><br>**‹**<br><br>**›**<br><br>**<**<br><br>**>** | `&#171;`<br><br>`&#187;`<br><br>`&#8249;`<br><br>`&#8250;`<br><br>`&lt;`<br><br>`&gt;` | `\u00AB`<br><br>`\u00BB`<br><br>`\u2039`<br><br>`\u203A`<br><br>`\u003C`<br><br>`\u003E`<br><br> | | | Omit from links or buttons that open another page or move to the next or previous step in a process. Also known as angle brackets, angular quote brackets, or guillemets. |
| Em dash | **—** | `&mdash;` | `\u2014` | <kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>-</kbd> | <kbd>Alt</kbd>+<kbd>0 1 5 1</kbd> | Avoid using dashes to separate text. If you must use dashes for this purpose — like this — use an em dash surrounded by spaces. |
| En dash | **–** | `&ndash;` | `\u2013` | <kbd>⌥ Option</kbd>+<kbd>-</kbd> | <kbd>Alt</kbd>+<kbd>0 1 5 0</kbd> | Use an en dash without spaces instead of a hyphen to indicate a range of values, such as numbers, times, and dates: “3–5 kg”, “8:00 AM–12:30 PM”, “10–17 Jan” |
| Hyphen | **-** | | | | | Use to represent negative numbers, or to avoid ambiguity in adjective-noun or noun-participle pairs. Example: “anti-inflammatory”; “5-mile walk.”<br><br>Omit in commonly understood terms and adverbs that end in *ly*: “frontend”, “greatly improved performance.”<br><br>Omit in the term “open source.” |
| Parentheses | **( )** | | | | | Use only to define acronyms or jargon: “Secure web connections are based on a technology called SSL (the secure sockets layer).”<br><br>Avoid other uses and instead rewrite the text, or use dashes or commas to set off the information. If parentheses are required: If the parenthetical is a complete, independent sentence, place the period inside the parentheses; if not, the period goes outside. |
When using the <kbd>Alt</kbd> keystrokes in Windows, use the numeric keypad, not the row of numbers above the alphabet, and be sure Num Lock is turned on.
---
......@@ -48,15 +113,15 @@ Only use the terms in the tables below.
| Deleted |
>**Example:**
Use `5 open issues` and don't use `5 pending issues`.
Use `5 open issues` and dont use `5 pending issues`.
#### Verbs (actions)
| Term | Use | Don't |
| Term | Use | Dont |
| ---- | --- | --- |
| Add | Add an issue | Don't use `create` or `new` |
| Add | Add an issue | Dont use `create` or `new` |
| View | View an open or closed issue ||
| Edit | Edit an open or closed issue | Don't use `update` |
| Edit | Edit an open or closed issue | Dont use `update` |
| Close | Close an open issue ||
| Re-open | Re-open a closed issue | There should never be a need to use `open` as a verb |
| Delete | Delete an open or closed issue ||
......@@ -67,7 +132,7 @@ When viewing a list of issues, there is a button that is labeled `Add`. Given th
![Add issue button](img/copy-form-addissuebutton.png)
The form should be titled `Add issue`. The submit button should be labeled `Submit`. Don't use `Add`, `Create`, `New`, or `Save changes`. The cancel button should be labeled `Cancel`. Don't use `Back`.
The form should be titled `Add issue`. The submit button should be labeled `Submit`. Don’t use `Add`, `Create`, `New`, or `Save changes`. The cancel button should be labeled `Cancel`. Don’t use `Back`.
![Add issue form](img/copy-form-addissueform.png)
......@@ -77,7 +142,7 @@ When in context of an issue, the affordance to edit it is labeled `Edit`. If the
![Edit issue button](img/copy-form-editissuebutton.png)
The form should be titled `Edit issue`. The submit button should be labeled `Save`. Don't use `Edit`, `Update`, `Submit`, or `Save changes`. The cancel button should be labeled `Cancel`. Don't use `Back`.
The form should be titled `Edit issue`. The submit button should be labeled `Save`. Don’t use `Edit`, `Update`, `Submit`, or `Save changes`. The cancel button should be labeled `Cancel`. Don’t use `Back`.
![Edit issue form](img/copy-form-editissueform.png)
......@@ -93,7 +158,7 @@ The form should be titled `Edit issue`. The submit button should be labeled `Sav
#### Verbs (actions)
| Term | Use | Don't |
| Term | Use | Dont |
| ---- | --- | --- |
| Add | Add a merge request | Do not use `create` or `new` |
| View | View an open or merged merge request ||
......@@ -105,7 +170,18 @@ The form should be titled `Edit issue`. The submit button should be labeled `Sav
### Comments & Discussions
#### Comment
A **comment** is a written piece of text that users of GitLab can create. Comments have the meta data of author and time stamp. Comments can be added in a variety of contexts, such as issues, merge requests, and discussions.
A **comment** is a written piece of text that users of GitLab can create. Comments have the meta data of author and timestamp. Comments can be added in a variety of contexts, such as issues, merge requests, and discussions.
#### Discussion
A **discussion** is a group of 1 or more comments. A discussion can include subdiscussions. Some discussions have the special capability of being able to be **resolved**. Both the comments in the discussion and the discussion itself can be resolved.
---
Portions of this page are modifications based on work created and shared by the [Android Open Source Project][android project] and used according to terms described in the [Creative Commons 2.5 Attribution License][creative commons].
#### Dicussion
A **discussion** is a group of 1 or more comments. A discussion can include sub discussions. Some discussions have the special capability of being able to be **resolved**. Both the comments in the discussion and the discussion itself can be resolved.
\ No newline at end of file
[material design]: https://material.io/guidelines/
[features]: https://about.gitlab.com/features/ "GitLab features page"
[products]: https://about.gitlab.com/products/ "GitLab products page"
[serial comma]: https://en.wikipedia.org/wiki/Serial_comma "“Serial comma” in Wikipedia"
[android project]: http://source.android.com/
[creative commons]: http://creativecommons.org/licenses/by/2.5/
\ No newline at end of file
......@@ -271,9 +271,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-15-stable gitlab
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-16-stable gitlab
**Note:** You can change `8-15-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
**Note:** You can change `8-16-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
......@@ -400,16 +400,10 @@ GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/). The
following command-line will install GitLab-Workhorse in `/home/git/gitlab-workhorse`
which is the recommended location.
cd /home/git/gitlab
sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
### Initialize Database and Activate Advanced Features
# Go to GitLab installation folder
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production
# Type 'yes' to create the database tables.
......
......@@ -88,7 +88,7 @@ It uses the [Fog library](http://fog.io/) to perform the upload.
In the example below we use Amazon S3 for storage, but Fog also lets you use
[other storage providers](http://fog.io/storage/). GitLab
[imports cloud drivers](https://gitlab.com/gitlab-org/gitlab-ce/blob/30f5b9a5b711b46f1065baf755e413ceced5646b/Gemfile#L88)
for AWS, OpenStack Swift and Rackspace as well. A local driver is
for AWS, Google, OpenStack Swift and Rackspace as well. A local driver is
[also available](#uploading-to-locally-mounted-shares).
For omnibus packages:
......
......@@ -11,12 +11,15 @@ guide links by version.
### 1. Stop server
sudo service gitlab stop
```bash
sudo service gitlab stop
```
### 2. Backup
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
......@@ -49,6 +52,8 @@ sudo gem install bundler --no-ri --no-rdoc
### 4. Get latest code
```bash
cd /home/git/gitlab
sudo -u git -H git fetch --all
sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
```
......@@ -56,6 +61,8 @@ sudo -u git -H git checkout -- db/schema.rb # local changes will be restored aut
For GitLab Community Edition:
```bash
cd /home/git/gitlab
sudo -u git -H git checkout 8-15-stable
```
......@@ -64,28 +71,12 @@ OR
For GitLab Enterprise Edition:
```bash
sudo -u git -H git checkout 8-15-stable-ee
```
### 5. Update gitlab-shell
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
sudo -u git -H git checkout v4.1.1
```
### 6. Update gitlab-workhorse
Install and compile gitlab-workhorse. This requires
[Go 1.5](https://golang.org/dl) which should already be on your system from
GitLab 8.1.
cd /home/git/gitlab
```bash
sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
sudo -u git -H git checkout 8-15-stable-ee
```
### 7. Install libs, migrations, etc.
### 5. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
......@@ -106,6 +97,27 @@ sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
```
### 6. Update gitlab-workhorse
Install and compile gitlab-workhorse. This requires
[Go 1.5](https://golang.org/dl) which should already be on your system from
GitLab 8.1.
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
```
### 7. Update gitlab-shell
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
sudo -u git -H git checkout v4.1.1
```
### 8. Update configuration files
#### New configuration options for `gitlab.yml`
......@@ -113,6 +125,8 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
```sh
cd /home/git/gitlab
git diff origin/8-14-stable:config/gitlab.yml.example origin/8-15-stable:config/gitlab.yml.example
```
......@@ -122,6 +136,8 @@ Configure Git to generate packfile bitmaps (introduced in Git 2.0) on
the GitLab server during `git gc`.
```sh
cd /home/git/gitlab
sudo -u git -H git config --global repack.writeBitmaps true
```
......@@ -130,6 +146,8 @@ sudo -u git -H git config --global repack.writeBitmaps true
Ensure you're still up-to-date with the latest NGINX configuration changes:
```sh
cd /home/git/gitlab
# For HTTPS configurations
git diff origin/8-14-stable:lib/support/nginx/gitlab-ssl origin/8-15-stable:lib/support/nginx/gitlab-ssl
......@@ -162,26 +180,42 @@ See [smtp_settings.rb.sample] as an example.
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```bash
cd /home/git/gitlab
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```
For Ubuntu 16.04.1 LTS:
sudo systemctl daemon-reload
```bash
sudo systemctl daemon-reload
```
### 9. Start application
sudo service gitlab start
sudo service nginx restart
```bash
sudo service gitlab start
sudo service nginx restart
```
### 10. Check application status
Check if GitLab and its environment are configured correctly:
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
```
To make sure you didn't miss anything run a more thorough check:
sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
```
If all items are green, then congratulations, the upgrade is complete!
......@@ -196,6 +230,7 @@ database migration (the backup is already migrated to the previous version).
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
```
......
# From 8.15 to 8.16
Make sure you view this update guide from the tag (version) of GitLab you would
like to install. In most cases this should be the highest numbered production
tag (without rc in it). You can select the tag in the version dropdown at the
top left corner of GitLab (below the menu bar).
If the highest number stable branch is unclear please check the
[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
guide links by version.
### 1. Stop server
```bash
sudo service gitlab stop
```
### 2. Backup
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
### 3. Update Ruby
We will continue supporting Ruby < 2.3 for the time being but we recommend you
upgrade to Ruby 2.3 if you're running a source installation, as this is the same
version that ships with our Omnibus package.
You can check which version you are running with `ruby -v`.
Download and compile Ruby:
```bash
mkdir /tmp/ruby && cd /tmp/ruby
curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz
echo 'a8db9ce7f9110320f33b8325200e3ecfbd2b534b ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
cd ruby-2.3.3
./configure --disable-install-rdoc
make
sudo make install
```
Install Bundler:
```bash
sudo gem install bundler --no-ri --no-rdoc
```
### 4. Get latest code
```bash
cd /home/git/gitlab
sudo -u git -H git fetch --all
sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
```
For GitLab Community Edition:
```bash
cd /home/git/gitlab
sudo -u git -H git checkout 8-16-stable
```
OR
For GitLab Enterprise Edition:
```bash
cd /home/git/gitlab
sudo -u git -H git checkout 8-16-stable-ee
```
### 5. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
# MySQL installations (note: the line below states '--without postgres')
sudo -u git -H bundle install --without postgres development test --deployment
# PostgreSQL installations (note: the line below states '--without mysql')
sudo -u git -H bundle install --without mysql development test --deployment
# Optional: clean up old gems
sudo -u git -H bundle clean
# Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
# Clean up assets and cache
sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
```
### 6. Update gitlab-workhorse
Install and compile gitlab-workhorse. This requires
[Go 1.5](https://golang.org/dl) which should already be on your system from
GitLab 8.1.
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
```
### 7. Update gitlab-shell
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
sudo -u git -H git checkout v4.1.1
```
### 8. Update configuration files
#### New configuration options for `gitlab.yml`
There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
```sh
cd /home/git/gitlab
git diff origin/8-15-stable:config/gitlab.yml.example origin/8-16-stable:config/gitlab.yml.example
```
#### Git configuration
Configure Git to generate packfile bitmaps (introduced in Git 2.0) on
the GitLab server during `git gc`.
```sh
cd /home/git/gitlab
sudo -u git -H git config --global repack.writeBitmaps true
```
#### Nginx configuration
Ensure you're still up-to-date with the latest NGINX configuration changes:
```sh
cd /home/git/gitlab
# For HTTPS configurations
git diff origin/8-15-stable:lib/support/nginx/gitlab-ssl origin/8-16-stable:lib/support/nginx/gitlab-ssl
# For HTTP configurations
git diff origin/8-15-stable:lib/support/nginx/gitlab origin/8-16-stable:lib/support/nginx/gitlab
```
If you are using Apache instead of NGINX please see the updated [Apache templates].
Also note that because Apache does not support upstreams behind Unix sockets you
will need to let gitlab-workhorse listen on a TCP port. You can do this
via [/etc/default/gitlab].
[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/lib/support/init.d/gitlab.default.example#L38
#### SMTP configuration
If you're installing from source and use SMTP to deliver mail, you will need to add the following line
to config/initializers/smtp_settings.rb:
```ruby
ActionMailer::Base.delivery_method = :smtp
```
See [smtp_settings.rb.sample] as an example.
[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/config/initializers/smtp_settings.rb.sample#L13
#### Init script
Ensure you're still up-to-date with the latest init script changes:
```bash
cd /home/git/gitlab
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```
For Ubuntu 16.04.1 LTS:
```bash
sudo systemctl daemon-reload
```
### 9. Start application
```bash
sudo service gitlab start
sudo service nginx restart
```
### 10. Check application status
Check if GitLab and its environment are configured correctly:
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
```
To make sure you didn't miss anything run a more thorough check:
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
```
If all items are green, then congratulations, the upgrade is complete!
## Things went south? Revert to previous version (8.15)
### 1. Revert the code to the previous version
Follow the [upgrade guide from 8.14 to 8.15](8.14-to-8.15.md), except for the
database migration (the backup is already migrated to the previous version).
### 2. Restore from the backup
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
```
If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
......@@ -14,6 +14,7 @@ user on the database version)
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
......@@ -32,28 +33,13 @@ current version with `cat VERSION`).
```bash
cd /home/git/gitlab
sudo -u git -H git fetch --all
sudo -u git -H git checkout -- Gemfile.lock db/schema.rb
sudo -u git -H git checkout LATEST_TAG -b LATEST_TAG
```
### 3. Update gitlab-shell to the corresponding version
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch
sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`cat /home/git/gitlab/GITLAB_SHELL_VERSION`
```
### 4. Update gitlab-workhorse to the corresponding version
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
```
### 5. Install libs, migrations, etc.
### 3. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
......@@ -74,6 +60,23 @@ sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
```
### 4. Update gitlab-workhorse to the corresponding version
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
```
### 5. Update gitlab-shell to the corresponding version
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`cat /home/git/gitlab/GITLAB_SHELL_VERSION`
```
### 6. Start application
```bash
......@@ -86,6 +89,8 @@ sudo service nginx restart
Check if GitLab and its environment are configured correctly:
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
```
......
......@@ -5,8 +5,7 @@ Import your projects from Gitea to GitLab with minimal effort.
## Overview
>**Note:**
As of Gitea `v1.0.0`, issue & pull-request comments cannot be imported! This is
a [known issue][issue-401] that should be fixed in a near-future.
This requires Gitea `v1.0.0` or newer.
- At its current state, Gitea importer can import:
- the repository description (GitLab 8.15+)
......@@ -76,5 +75,3 @@ If you want, you can import all your Gitea projects in one go by hitting
You can also choose a different name for the project and a different namespace,
if you have the privileges to do so.
[issue-401]: https://github.com/go-gitea/gitea/issues/401
......@@ -73,7 +73,7 @@ In all of the below cases, the notification will be sent to:
...with notification level "Participating" or higher
- Watchers: users with notification level "Watch"
- Watchers: users with notification level "Watch" (however successful pipeline would be off for watchers)
- Subscribers: anyone who manually subscribed to the issue/merge request
- Custom: Users with notification level "custom" who turned on notifications for any of the events present in the table below
......
......@@ -114,8 +114,10 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end
step 'I click link "Import team from another project"' do
page.within '.users-project-form' do
click_link "Import"
end
end
When 'I submit "Website" project for import team' do
project = Project.find_by(name: "Website")
......
......@@ -14,7 +14,11 @@ module API
end
# Retain 405 error rather than a 500 error for Grape 0.15.0+.
# See: https://github.com/ruby-grape/grape/commit/252bfd27c320466ec3c0751812cf44245e97e5de
# https://github.com/ruby-grape/grape/blob/a3a28f5b5dfbb2797442e006dbffd750b27f2a76/UPGRADING.md#changes-to-method-not-allowed-routes
rescue_from Grape::Exceptions::MethodNotAllowed do |e|
error! e.message, e.status, e.headers
end
rescue_from Grape::Exceptions::Base do |e|
error! e.message, e.status, e.headers
end
......
......@@ -28,6 +28,8 @@ module API
protocol = params[:protocol]
actor.update_last_used_at if actor.is_a?(Key)
access =
if wiki?
Gitlab::GitAccessWiki.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
......@@ -61,6 +63,8 @@ module API
status 200
key = Key.find(params[:key_id])
key.update_last_used_at
token_handler = Gitlab::LfsToken.new(key)
{
......@@ -103,7 +107,9 @@ module API
key = Key.find_by(id: params[:key_id])
unless key
if key
key.update_last_used_at
else
return { 'success' => false, 'message' => 'Could not find the given key' }
end
......
......@@ -91,10 +91,11 @@ module API
authenticated_as_admin!
# Filter out params which are used later
identity_attrs = params.slice(:provider, :extern_uid)
user_params = declared_params(include_missing: false)
identity_attrs = user_params.slice(:provider, :extern_uid)
confirm = params.delete(:confirm)
user = User.new(declared_params(include_missing: false))
user = User.new(user_params.except(:extern_uid, :provider))
user.skip_confirmation! unless confirm
if identity_attrs.any?
......@@ -159,11 +160,7 @@ module API
end
end
# Delete already handled parameters
user_params.delete(:extern_uid)
user_params.delete(:provider)
if user.update_attributes(user_params)
if user.update_attributes(user_params.except(:extern_uid, :provider))
present user, with: Entities::UserPublic
else
render_validation_error!(user)
......
......@@ -105,7 +105,7 @@ module Ci
break
elsif s.scan(/</)
@out << '&lt;'
elsif s.scan(/\n/)
elsif s.scan(/\r?\n/)
@out << '<br>'
else
@out << s.scan(/./m)
......
......@@ -8,6 +8,16 @@ module Ci
rack_response({ 'message' => '404 Not found' }.to_json, 404)
end
# Retain 405 error rather than a 500 error for Grape 0.15.0+.
# https://github.com/ruby-grape/grape/blob/a3a28f5b5dfbb2797442e006dbffd750b27f2a76/UPGRADING.md#changes-to-method-not-allowed-routes
rescue_from Grape::Exceptions::MethodNotAllowed do |e|
error! e.message, e.status, e.headers
end
rescue_from Grape::Exceptions::Base do |e|
error! e.message, e.status, e.headers
end
rescue_from :all do |exception|
handle_api_exception(exception)
end
......
......@@ -34,21 +34,21 @@ module Gitlab
def allowed?
if ldap_user
unless ldap_config.active_directory
user.activate if user.ldap_blocked?
unblock_user(user, 'is available again') if user.ldap_blocked?
return true
end
# Block user in GitLab if he/she was blocked in AD
if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
user.ldap_block
block_user(user, 'is disabled in Active Directory')
false
else
user.activate if user.ldap_blocked?
unblock_user(user, 'is not disabled anymore') if user.ldap_blocked?
true
end
else
# Block the user if they no longer exist in LDAP/AD
user.ldap_block
block_user(user, 'does not exist anymore')
false
end
end
......@@ -64,6 +64,24 @@ module Gitlab
def ldap_user
@ldap_user ||= Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter)
end
def block_user(user, reason)
user.ldap_block
Gitlab::AppLogger.info(
"LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " +
"blocking Gitlab user \"#{user.name}\" (#{user.email})"
)
end
def unblock_user(user, reason)
user.activate
Gitlab::AppLogger.info(
"LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " +
"unblocking Gitlab user \"#{user.name}\" (#{user.email})"
)
end
end
end
end
......@@ -25,7 +25,7 @@ module Gitlab
end
def get_raw(key)
auth_hash.extra[:raw_info][key]
auth_hash.extra[:raw_info][key] if auth_hash.extra
end
def ldap_config
......
......@@ -107,7 +107,7 @@ module Gitlab
end
def attributes
options['attributes']
default_attributes.merge(options['attributes'])
end
def timeout
......@@ -130,6 +130,16 @@ module Gitlab
end
end
def default_attributes
{
'username' => %w(uid userid sAMAccountName),
'email' => %w(mail email userPrincipalName),
'name' => 'cn',
'first_name' => 'givenName',
'last_name' => 'sn'
}
end
protected
def base_options
......
......@@ -28,7 +28,7 @@ module Gitlab
end
def name
attribute_value(:name)
attribute_value(:name).first
end
def uid
......@@ -62,14 +62,12 @@ module Gitlab
# this method looks for 'mail', 'email' and 'userPrincipalName' and
# returns the first with a value.
def attribute_value(attribute)
attributes = Array(config.attributes[attribute.to_sym])
attributes = Array(config.attributes[attribute.to_s])
selected_attr = attributes.find { |attr| entry.respond_to?(attr) }
return nil unless selected_attr
# Some LDAP attributes return an array,
# even if it is a single value (like 'cn')
Array(entry.public_send(selected_attr)).first
entry.public_send(selected_attr)
end
end
end
......
......@@ -70,9 +70,13 @@ module Gitlab
def tag_endpoint(trans, env)
endpoint = env[ENDPOINT_KEY]
# endpoint.route is nil in the case of a 405 response
if endpoint.route
path = endpoint_paths_cache[endpoint.route.request_method][endpoint.route.path]
trans.action = "Grape##{endpoint.route.request_method} #{path}"
end
end
private
......
......@@ -31,7 +31,7 @@ describe Projects::GroupLinksController do
it 'redirects to project group links page' do
expect(response).to redirect_to(
namespace_project_group_links_path(project.namespace, project)
namespace_project_settings_members_path(project.namespace, project)
)
end
end
......@@ -62,7 +62,7 @@ describe Projects::GroupLinksController do
it 'redirects to project group links page' do
expect(response).to redirect_to(
namespace_project_group_links_path(project.namespace, project)
namespace_project_settings_members_path(project.namespace, project)
)
end
end
......@@ -76,7 +76,7 @@ describe Projects::GroupLinksController do
it 'redirects to project group links page' do
expect(response).to redirect_to(
namespace_project_group_links_path(project.namespace, project)
namespace_project_settings_members_path(project.namespace, project)
)
expect(flash[:alert]).to eq('Please select a group.')
end
......
......@@ -5,11 +5,11 @@ describe Projects::ProjectMembersController do
let(:project) { create(:empty_project, :public, :access_requestable) }
describe 'GET index' do
it 'renders index with 200 status code' do
it 'should have the settings/members address with a 302 status code' do
get :index, namespace_id: project.namespace, project_id: project
expect(response).to have_http_status(200)
expect(response).to render_template(:index)
expect(response).to have_http_status(302)
expect(response.location).to include namespace_project_settings_members_path(project.namespace, project)
end
end
......@@ -44,7 +44,7 @@ describe Projects::ProjectMembersController do
access_level: Gitlab::Access::GUEST
expect(response).to set_flash.to 'Users were successfully added.'
expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project))
expect(response).to redirect_to(namespace_project_settings_members_path(project.namespace, project))
end
it 'adds no user to members' do
......@@ -56,7 +56,7 @@ describe Projects::ProjectMembersController do
access_level: Gitlab::Access::GUEST
expect(response).to set_flash.to 'No users or groups specified.'
expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project))
expect(response).to redirect_to(namespace_project_settings_members_path(project.namespace, project))
end
end
end
......@@ -99,7 +99,7 @@ describe Projects::ProjectMembersController do
id: member
expect(response).to redirect_to(
namespace_project_project_members_path(project.namespace, project)
namespace_project_settings_members_path(project.namespace, project)
)
expect(project.members).not_to include member
end
......@@ -259,7 +259,7 @@ describe Projects::ProjectMembersController do
expect(project.team_members).to include member
expect(response).to set_flash.to 'Successfully imported'
expect(response).to redirect_to(
namespace_project_project_members_path(project.namespace, project)
namespace_project_settings_members_path(project.namespace, project)
)
end
end
......
require('spec_helper')
describe Projects::Settings::MembersController do
let(:project) { create(:empty_project, :public, :access_requestable) }
describe 'GET show' do
it 'renders show with 200 status code' do
get :show, namespace_id: project.namespace, project_id: project
expect(response).to have_http_status(200)
expect(response).to render_template(:show)
end
end
end
require 'rails_helper'
feature 'Issue markdown toolbar', feature: true, js: true do
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
before do
login_as(user)
visit namespace_project_issue_path(project.namespace, project, issue)
end
it "doesn't include first new line when adding bold" do
find('#note_note').native.send_keys('test')
find('#note_note').native.send_key(:enter)
find('#note_note').native.send_keys('bold')
page.evaluate_script('document.querySelectorAll(".js-main-target-form #note_note")[0].setSelectionRange(4, 9)')
first('.toolbar-btn').click
expect(find('#note_note')[:value]).to eq("test\n**bold**\n")
end
it "doesn't include first new line when adding underline" do
find('#note_note').native.send_keys('test')
find('#note_note').native.send_key(:enter)
find('#note_note').native.send_keys('underline')
page.evaluate_script('document.querySelectorAll(".js-main-target-form #note_note")[0].setSelectionRange(4, 50)')
find('.toolbar-btn:nth-child(2)').click
expect(find('#note_note')[:value]).to eq("test\n*underline*\n")
end
end
......@@ -619,14 +619,18 @@ describe 'Issues', feature: true do
end
it 'adds due date to issue' do
date = Date.today.at_beginning_of_month + 2.days
page.within '.due_date' do
click_link 'Edit'
page.within '.ui-datepicker-calendar' do
first('.ui-state-default').click
click_link date.day
end
expect(page).to have_no_content 'None'
wait_for_ajax
expect(find('.value').text).to have_content date.strftime('%b %-d, %Y')
end
end
......@@ -638,6 +642,8 @@ describe 'Issues', feature: true do
first('.ui-state-default').click
end
wait_for_ajax
expect(page).to have_no_content 'No due date'
click_link 'remove due date'
......
......@@ -4,6 +4,8 @@ require 'spec_helper'
# message to be shown by JavaScript when the source branch was deleted.
# Please do not remove "js: true".
describe 'Deleted source branch', feature: true, js: true do
include WaitForAjax
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
......@@ -13,7 +15,8 @@ describe 'Deleted source branch', feature: true, js: true do
merge_request.update!(source_branch: 'this-branch-does-not-exist')
visit namespace_project_merge_request_path(
merge_request.project.namespace,
merge_request.project, merge_request
merge_request.project,
merge_request
)
end
......@@ -23,11 +26,17 @@ describe 'Deleted source branch', feature: true, js: true do
)
end
it 'hides Discussion, Commits and Changes tabs' do
it 'still contains Discussion, Commits and Changes tabs' do
within '.merge-request-details' do
expect(page).to have_no_content('Discussion')
expect(page).to have_no_content('Commits')
expect(page).to have_no_content('Changes')
expect(page).to have_content('Discussion')
expect(page).to have_content('Commits')
expect(page).to have_content('Changes')
end
click_on 'Changes'
wait_for_ajax
expect(page).to have_selector('.diffs.tab-pane .nothing-here-block')
expect(page).to have_content('Nothing to merge from this-branch-does-not-exist into feature')
end
end
......@@ -14,10 +14,10 @@ feature 'Project group links', feature: true, js: true do
context 'setting an expiration date for a group link' do
before do
visit namespace_project_group_links_path(project.namespace, project)
visit namespace_project_settings_members_path(project.namespace, project)
select2 group.id, from: '#link_group_id'
fill_in 'expires_at', with: (Time.current + 4.5.days).strftime('%Y-%m-%d')
fill_in 'expires_at_groups', with: (Time.current + 4.5.days).strftime('%Y-%m-%d')
page.find('body').click
click_on 'Share'
end
......
......@@ -27,7 +27,7 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects "bug" template' do
select_template 'bug'
wait_for_ajax
preview_template
assert_template
save_changes
end
......@@ -35,8 +35,7 @@ feature 'issuable templates', feature: true, js: true do
select_template 'bug'
wait_for_ajax
select_option 'No template'
wait_for_ajax
preview_template('')
assert_template('')
save_changes('')
end
......@@ -44,9 +43,9 @@ feature 'issuable templates', feature: true, js: true do
select_template 'bug'
wait_for_ajax
find_field('issue_description').send_keys(description_addition)
preview_template(template_content + description_addition)
assert_template(template_content + description_addition)
select_option 'Reset template'
preview_template
assert_template
save_changes
end
......@@ -77,7 +76,7 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects "bug" template' do
select_template 'bug'
wait_for_ajax
preview_template("#{template_content}")
assert_template("#{template_content}")
save_changes
end
end
......@@ -95,7 +94,7 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects "feature-proposal" template' do
select_template 'feature-proposal'
wait_for_ajax
preview_template
assert_template
save_changes
end
end
......@@ -122,17 +121,15 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects template' do
select_template 'feature-proposal'
wait_for_ajax
preview_template
assert_template
save_changes
end
end
end
end
def preview_template(expected_content = template_content)
click_link 'Preview'
expect(page).to have_content expected_content
click_link 'Write'
def assert_template(expected_content = template_content)
expect(find('textarea')['value']).to eq(expected_content)
end
def save_changes(expected_content = template_content)
......
......@@ -11,10 +11,10 @@ feature 'Projects > Members > Anonymous user sees members', feature: true do
end
scenario "anonymous user visits the project's members page and sees the list of members" do
visit namespace_project_project_members_path(project.namespace, project)
visit namespace_project_settings_members_path(project.namespace, project)
expect(current_path).to eq(
namespace_project_project_members_path(project.namespace, project))
namespace_project_settings_members_path(project.namespace, project))
expect(page).to have_content(user.name)
end
end
......@@ -12,7 +12,7 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t
@group_link = create(:project_group_link, project: project, group: group)
login_as(user)
visit namespace_project_project_members_path(project.namespace, project)
visit namespace_project_settings_members_path(project.namespace, project)
end
it 'updates group access level' do
......@@ -24,7 +24,7 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t
wait_for_ajax
visit namespace_project_project_members_path(project.namespace, project)
visit namespace_project_settings_members_path(project.namespace, project)
expect(first('.group_member')).to have_content('Guest')
end
......
......@@ -19,7 +19,7 @@ feature 'Projects members', feature: true do
context 'with a group invitee' do
before do
group_invitee
visit namespace_project_project_members_path(project.namespace, project)
visit namespace_project_settings_members_path(project.namespace, project)
end
scenario 'does not appear in the project members page' do
......@@ -33,7 +33,7 @@ feature 'Projects members', feature: true do
before do
group_invitee
project_invitee
visit namespace_project_project_members_path(project.namespace, project)
visit namespace_project_settings_members_path(project.namespace, project)
end
scenario 'shows the project invitee, the project developer, and the group owner' do
......@@ -54,7 +54,7 @@ feature 'Projects members', feature: true do
context 'with a group requester' do
before do
group.request_access(group_requester)
visit namespace_project_project_members_path(project.namespace, project)
visit namespace_project_settings_members_path(project.namespace, project)
end
scenario 'does not appear in the project members page' do
......@@ -68,7 +68,7 @@ feature 'Projects members', feature: true do
before do
group.request_access(group_requester)
project.request_access(project_requester)
visit namespace_project_project_members_path(project.namespace, project)
visit namespace_project_settings_members_path(project.namespace, project)
end
scenario 'shows the project requester, the project developer, and the group owner' do
......
......@@ -14,15 +14,15 @@ feature 'Projects > Members > Master adds member with expiration date', feature:
login_as(master)
end
scenario 'expiration date is displayed in the members list' do
scenario 'expiration date is displayed in the members list', js: true do
travel_to Time.zone.parse('2016-08-06 08:00') do
visit namespace_project_project_members_path(project.namespace, project)
visit namespace_project_settings_members_path(project.namespace, project)
page.within '.users-project-form' do
select2(new_member.id, from: '#user_ids', multiple: true)
fill_in 'expires_at', with: '2016-08-10'
click_on 'Add to project'
end
find('.users-project-form').click
click_on 'Add to project'
page.within "#project_member_#{new_member.project_members.first.id}" do
expect(page).to have_content('Expires in 4 days')
......
......@@ -39,7 +39,7 @@ feature 'Projects > Members > User requests access', feature: true do
open_project_settings_menu
click_link 'Members'
visit namespace_project_project_members_path(project.namespace, project)
visit namespace_project_settings_members_path(project.namespace, project)
page.within('.content') do
expect(page).not_to have_content(user.name)
end
......
......@@ -82,8 +82,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/project_members" do
subject { namespace_project_project_members_path(project.namespace, project) }
describe "GET /:project_path/settings/members" do
subject { namespace_project_settings_members_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
......
......@@ -82,8 +82,8 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_denied_for(:visitor) }
end
describe "GET /:project_path/project_members" do
subject { namespace_project_project_members_path(project.namespace, project) }
describe "GET /:project_path/settings/members" do
subject { namespace_project_settings_members_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
......
......@@ -82,8 +82,8 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for(:visitor) }
end
describe "GET /:project_path/project_members" do
subject { namespace_project_project_members_path(project.namespace, project) }
describe "GET /:project_path/settings/members" do
subject { namespace_project_settings_members_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) }
......
{
"plugins": ["jasmine"],
"env": {
"jasmine": true
},
"extends": "plugin:jasmine/recommended",
"globals": {
"appendLoadFixtures": false,
"appendLoadStyleFixtures": false,
"appendSetFixtures": false,
"appendSetStyleFixtures": false,
"getJSONFixture": false,
"loadFixtures": false,
"loadJSONFixtures": false,
"loadStyleFixtures": false,
"preloadFixtures": false,
"preloadStyleFixtures": false,
"readFixtures": false,
"sandbox": false,
"setFixtures": false,
"setStyleFixtures": false,
"spyOnEvent": false
},
"plugins": ["jasmine"],
"rules": {
"prefer-arrow-callback": 0,
"func-names": 0
},
"globals": {
"fixture": false,
"spyOnEvent": false
}
}
......@@ -13,10 +13,10 @@
(index, element) => element.innerText.indexOf(searchText) > -1,
).first();
fixture.preload(FIXTURE);
preloadFixtures(FIXTURE);
beforeEach(function () {
fixture.load(FIXTURE);
loadFixtures(FIXTURE);
this.abuseReports = new global.AbuseReports();
messages = $('.abuse-reports .message');
});
......
......@@ -7,7 +7,7 @@
(() => {
window.gon || (window.gon = {});
const fixtureTemplate = 'event_filter.html';
const fixtureTemplate = 'static/event_filter.html.raw';
const filters = [
{
id: 'all',
......@@ -35,7 +35,7 @@
describe('Activities', () => {
beforeEach(() => {
fixture.load(fixtureTemplate);
loadFixtures(fixtureTemplate);
new gl.Activities();
});
......
......@@ -34,9 +34,9 @@
};
describe('AwardsHandler', function() {
fixture.preload('issues/open-issue.html.raw');
preloadFixtures('issues/open-issue.html.raw');
beforeEach(function() {
fixture.load('issues/open-issue.html.raw');
loadFixtures('issues/open-issue.html.raw');
awardsHandler = new AwardsHandler;
spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) {
return function(url, emoji, cb) {
......
......@@ -6,7 +6,7 @@
describe('Autosize behavior', function() {
var load;
beforeEach(function() {
return fixture.set('<textarea class="js-autosize" style="resize: vertical"></textarea>');
return setFixtures('<textarea class="js-autosize" style="resize: vertical"></textarea>');
});
it('does not overwrite the resize property', function() {
load();
......
......@@ -5,9 +5,9 @@
(function() {
describe('Quick Submit behavior', function() {
var keydownEvent;
fixture.preload('behaviors/quick_submit.html');
preloadFixtures('static/behaviors/quick_submit.html.raw');
beforeEach(function() {
fixture.load('behaviors/quick_submit.html');
loadFixtures('static/behaviors/quick_submit.html.raw');
$('form').submit(function(e) {
// Prevent a form submit from moving us off the testing page
return e.preventDefault();
......
......@@ -4,9 +4,9 @@
(function() {
describe('requiresInput', function() {
fixture.preload('behaviors/requires_input.html');
preloadFixtures('static/behaviors/requires_input.html.raw');
beforeEach(function() {
return fixture.load('behaviors/requires_input.html');
return loadFixtures('static/behaviors/requires_input.html.raw');
});
it('disables submit when any field is required', function() {
$('.js-requires-input').requiresInput();
......
......@@ -2,10 +2,10 @@
(() => {
describe('Linked Tabs', () => {
fixture.preload('linked_tabs');
preloadFixtures('static/linked_tabs.html.raw');
beforeEach(() => {
fixture.load('linked_tabs');
loadFixtures('static/linked_tabs.html.raw');
});
describe('when is initialized', () => {
......
......@@ -17,10 +17,10 @@ describe('Build', () => {
offset: BUILD_TRACE.length, n_open_tags: 0, fg_color: null, bg_color: null, style_mask: 0,
}));
fixture.preload('builds/build-with-artifacts.html.raw');
preloadFixtures('builds/build-with-artifacts.html.raw');
beforeEach(() => {
fixture.load('builds/build-with-artifacts.html.raw');
loadFixtures('builds/build-with-artifacts.html.raw');
spyOn($, 'ajax');
});
......
......@@ -7,7 +7,7 @@
((global) => {
describe('Dashboard', () => {
const fixtureTemplate = 'dashboard.html';
const fixtureTemplate = 'static/dashboard.html.raw';
function todosCountText() {
return $('.js-todos-count').text();
......@@ -17,9 +17,9 @@
$(document).trigger('todo:toggle', newCount);
}
fixture.preload(fixtureTemplate);
preloadFixtures(fixtureTemplate);
beforeEach(() => {
fixture.load(fixtureTemplate);
loadFixtures(fixtureTemplate);
new global.Sidebar();
});
......
......@@ -2,10 +2,10 @@
//= require environments/components/environment_actions
describe('Actions Component', () => {
fixture.preload('environments/element.html');
preloadFixtures('static/environments/element.html.raw');
beforeEach(() => {
fixture.load('environments/element.html');
loadFixtures('static/environments/element.html.raw');
});
it('should render a dropdown with the provided actions', () => {
......
......@@ -2,9 +2,9 @@
//= require environments/components/environment_external_url
describe('External URL Component', () => {
fixture.preload('environments/element.html');
preloadFixtures('static/environments/element.html.raw');
beforeEach(() => {
fixture.load('environments/element.html');
loadFixtures('static/environments/element.html.raw');
});
it('should link to the provided externalUrl prop', () => {
......
......@@ -3,9 +3,9 @@
//= require environments/components/environment_item
describe('Environment item', () => {
fixture.preload('environments/table.html');
preloadFixtures('static/environments/table.html.raw');
beforeEach(() => {
fixture.load('environments/table.html');
loadFixtures('static/environments/table.html.raw');
});
describe('When item is folder', () => {
......
//= require vue
//= require environments/components/environment_rollback
describe('Rollback Component', () => {
fixture.preload('environments/element.html');
preloadFixtures('static/environments/element.html.raw');
const retryURL = 'https://gitlab.com/retry';
beforeEach(() => {
fixture.load('environments/element.html');
loadFixtures('static/environments/element.html.raw');
});
it('Should link to the provided retryUrl', () => {
......
//= require vue
//= require environments/components/environment_stop
describe('Stop Component', () => {
fixture.preload('environments/element.html');
preloadFixtures('static/environments/element.html.raw');
let stopURL;
let component;
beforeEach(() => {
fixture.load('environments/element.html');
loadFixtures('static/environments/element.html.raw');
stopURL = '/stop';
component = new window.gl.environmentsList.StopComponent({
......
......@@ -6,7 +6,7 @@
describe('jQuery extensions', function() {
describe('disable', function() {
beforeEach(function() {
return fixture.set('<input type="text" />');
return setFixtures('<input type="text" />');
});
it('adds the disabled attribute', function() {
var $input;
......@@ -23,7 +23,7 @@
});
return describe('enable', function() {
beforeEach(function() {
return fixture.set('<input type="text" disabled="disabled" class="disabled" />');
return setFixtures('<input type="text" disabled="disabled" class="disabled" />');
});
it('removes the disabled attribute', function() {
var $input;
......
[{"id":9,"description":"","default_branch":null,"tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:root/test.git","http_url_to_repo":"http://localhost:3000/root/test.git","web_url":"http://localhost:3000/root/test","owner":{"name":"Administrator","username":"root","id":1,"state":"active","avatar_url":"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon","web_url":"http://localhost:3000/u/root"},"name":"test","name_with_namespace":"Administrator / test","path":"test","path_with_namespace":"root/test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-14T19:08:05.364Z","last_activity_at":"2016-01-14T19:08:07.418Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":1,"name":"root","path":"root","owner_id":1,"created_at":"2016-01-13T20:19:44.439Z","updated_at":"2016-01-13T20:19:44.439Z","description":"","avatar":null},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":0,"permissions":{"project_access":null,"group_access":null}},{"id":8,"description":"Voluptatem quae nulla eius numquam ullam voluptatibus quia modi.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:h5bp/html5-boilerplate.git","http_url_to_repo":"http://localhost:3000/h5bp/html5-boilerplate.git","web_url":"http://localhost:3000/h5bp/html5-boilerplate","name":"Html5 Boilerplate","name_with_namespace":"H5bp / Html5 Boilerplate","path":"html5-boilerplate","path_with_namespace":"h5bp/html5-boilerplate","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:57.525Z","last_activity_at":"2016-01-13T20:27:57.280Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":5,"name":"H5bp","path":"h5bp","owner_id":null,"created_at":"2016-01-13T20:19:57.239Z","updated_at":"2016-01-13T20:19:57.239Z","description":"Tempore accusantium possimus aut libero.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":10,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":7,"description":"Modi odio mollitia dolorem qui.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:twitter/typeahead-js.git","http_url_to_repo":"http://localhost:3000/twitter/typeahead-js.git","web_url":"http://localhost:3000/twitter/typeahead-js","name":"Typeahead.Js","name_with_namespace":"Twitter / Typeahead.Js","path":"typeahead-js","path_with_namespace":"twitter/typeahead-js","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:56.212Z","last_activity_at":"2016-01-13T20:27:51.496Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":true,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":6,"description":"Omnis asperiores ipsa et beatae quidem necessitatibus quia.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:twitter/flight.git","http_url_to_repo":"http://localhost:3000/twitter/flight.git","web_url":"http://localhost:3000/twitter/flight","name":"Flight","name_with_namespace":"Twitter / Flight","path":"flight","path_with_namespace":"twitter/flight","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:54.754Z","last_activity_at":"2016-01-13T20:27:50.502Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":true,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":5,"description":"Voluptatem commodi voluptate placeat architecto beatae illum dolores fugiat.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-test.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-test.git","web_url":"http://localhost:3000/gitlab-org/gitlab-test","name":"Gitlab Test","name_with_namespace":"Gitlab Org / Gitlab Test","path":"gitlab-test","path_with_namespace":"gitlab-org/gitlab-test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:53.202Z","last_activity_at":"2016-01-13T20:27:41.626Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":4,"description":"Aut molestias quas est ut aperiam officia quod libero.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-shell.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-shell.git","web_url":"http://localhost:3000/gitlab-org/gitlab-shell","name":"Gitlab Shell","name_with_namespace":"Gitlab Org / Gitlab Shell","path":"gitlab-shell","path_with_namespace":"gitlab-org/gitlab-shell","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:51.882Z","last_activity_at":"2016-01-13T20:27:35.678Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":20,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":3,"description":"Excepturi molestiae quia repellendus omnis est illo illum eligendi.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ci.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ci.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ci","name":"Gitlab Ci","name_with_namespace":"Gitlab Org / Gitlab Ci","path":"gitlab-ci","path_with_namespace":"gitlab-org/gitlab-ci","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:50.346Z","last_activity_at":"2016-01-13T20:27:30.115Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":3,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":2,"description":"Adipisci quaerat dignissimos enim sed ipsam dolorem quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":10,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ce.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ce.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ce","name":"Gitlab Ce","name_with_namespace":"Gitlab Org / Gitlab Ce","path":"gitlab-ce","path_with_namespace":"gitlab-org/gitlab-ce","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:49.065Z","last_activity_at":"2016-01-13T20:26:58.454Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":30,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":1,"description":"Vel voluptatem maxime saepe ex quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:documentcloud/underscore.git","http_url_to_repo":"http://localhost:3000/documentcloud/underscore.git","web_url":"http://localhost:3000/documentcloud/underscore","name":"Underscore","name_with_namespace":"Documentcloud / Underscore","path":"underscore","path_with_namespace":"documentcloud/underscore","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:45.862Z","last_activity_at":"2016-01-13T20:25:03.106Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":2,"name":"Documentcloud","path":"documentcloud","owner_id":null,"created_at":"2016-01-13T20:19:44.464Z","updated_at":"2016-01-13T20:19:44.464Z","description":"Aut impedit perferendis fuga et ipsa repellat cupiditate et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}}]
[{
"id": 9,
"description": "",
"default_branch": null,
"tag_list": [],
"public": true,
"archived": false,
"visibility_level": 20,
"ssh_url_to_repo": "phil@localhost:root/test.git",
"http_url_to_repo": "http://localhost:3000/root/test.git",
"web_url": "http://localhost:3000/root/test",
"owner": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon",
"web_url": "http://localhost:3000/u/root"
},
"name": "test",
"name_with_namespace": "Administrator / test",
"path": "test",
"path_with_namespace": "root/test",
"issues_enabled": true,
"merge_requests_enabled": true,
"wiki_enabled": true,
"builds_enabled": true,
"snippets_enabled": false,
"created_at": "2016-01-14T19:08:05.364Z",
"last_activity_at": "2016-01-14T19:08:07.418Z",
"shared_runners_enabled": true,
"creator_id": 1,
"namespace": {
"id": 1,
"name": "root",
"path": "root",
"owner_id": 1,
"created_at": "2016-01-13T20:19:44.439Z",
"updated_at": "2016-01-13T20:19:44.439Z",
"description": "",
"avatar": null
},
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
"only_allow_merge_if_build_succeeds": false,
"open_issues_count": 0,
"permissions": {
"project_access": null,
"group_access": null
}
}, {
"id": 8,
"description": "Voluptatem quae nulla eius numquam ullam voluptatibus quia modi.",
"default_branch": "master",
"tag_list": [],
"public": false,
"archived": false,
"visibility_level": 0,
"ssh_url_to_repo": "phil@localhost:h5bp/html5-boilerplate.git",
"http_url_to_repo": "http://localhost:3000/h5bp/html5-boilerplate.git",
"web_url": "http://localhost:3000/h5bp/html5-boilerplate",
"name": "Html5 Boilerplate",
"name_with_namespace": "H5bp / Html5 Boilerplate",
"path": "html5-boilerplate",
"path_with_namespace": "h5bp/html5-boilerplate",
"issues_enabled": true,
"merge_requests_enabled": true,
"wiki_enabled": true,
"builds_enabled": true,
"snippets_enabled": false,
"created_at": "2016-01-13T20:19:57.525Z",
"last_activity_at": "2016-01-13T20:27:57.280Z",
"shared_runners_enabled": true,
"creator_id": 1,
"namespace": {
"id": 5,
"name": "H5bp",
"path": "h5bp",
"owner_id": null,
"created_at": "2016-01-13T20:19:57.239Z",
"updated_at": "2016-01-13T20:19:57.239Z",
"description": "Tempore accusantium possimus aut libero.",
"avatar": {
"url": null
}
},
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
"only_allow_merge_if_build_succeeds": false,
"open_issues_count": 5,
"permissions": {
"project_access": {
"access_level": 10,
"notification_level": 3
},
"group_access": {
"access_level": 50,
"notification_level": 3
}
}
}, {
"id": 7,
"description": "Modi odio mollitia dolorem qui.",
"default_branch": "master",
"tag_list": [],
"public": false,
"archived": false,
"visibility_level": 0,
"ssh_url_to_repo": "phil@localhost:twitter/typeahead-js.git",
"http_url_to_repo": "http://localhost:3000/twitter/typeahead-js.git",
"web_url": "http://localhost:3000/twitter/typeahead-js",
"name": "Typeahead.Js",
"name_with_namespace": "Twitter / Typeahead.Js",
"path": "typeahead-js",
"path_with_namespace": "twitter/typeahead-js",
"issues_enabled": true,
"merge_requests_enabled": true,
"wiki_enabled": true,
"builds_enabled": true,
"snippets_enabled": false,
"created_at": "2016-01-13T20:19:56.212Z",
"last_activity_at": "2016-01-13T20:27:51.496Z",
"shared_runners_enabled": true,
"creator_id": 1,
"namespace": {
"id": 4,
"name": "Twitter",
"path": "twitter",
"owner_id": null,
"created_at": "2016-01-13T20:19:54.480Z",
"updated_at": "2016-01-13T20:19:54.480Z",
"description": "Id voluptatem ipsa maiores omnis repudiandae et et.",
"avatar": {
"url": null
}
},
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
"only_allow_merge_if_build_succeeds": true,
"open_issues_count": 4,
"permissions": {
"project_access": null,
"group_access": {
"access_level": 10,
"notification_level": 3
}
}
}, {
"id": 6,
"description": "Omnis asperiores ipsa et beatae quidem necessitatibus quia.",
"default_branch": "master",
"tag_list": [],
"public": true,
"archived": false,
"visibility_level": 20,
"ssh_url_to_repo": "phil@localhost:twitter/flight.git",
"http_url_to_repo": "http://localhost:3000/twitter/flight.git",
"web_url": "http://localhost:3000/twitter/flight",
"name": "Flight",
"name_with_namespace": "Twitter / Flight",
"path": "flight",
"path_with_namespace": "twitter/flight",
"issues_enabled": true,
"merge_requests_enabled": true,
"wiki_enabled": true,
"builds_enabled": true,
"snippets_enabled": false,
"created_at": "2016-01-13T20:19:54.754Z",
"last_activity_at": "2016-01-13T20:27:50.502Z",
"shared_runners_enabled": true,
"creator_id": 1,
"namespace": {
"id": 4,
"name": "Twitter",
"path": "twitter",
"owner_id": null,
"created_at": "2016-01-13T20:19:54.480Z",
"updated_at": "2016-01-13T20:19:54.480Z",
"description": "Id voluptatem ipsa maiores omnis repudiandae et et.",
"avatar": {
"url": null
}
},
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
"only_allow_merge_if_build_succeeds": true,
"open_issues_count": 4,
"permissions": {
"project_access": null,
"group_access": {
"access_level": 10,
"notification_level": 3
}
}
}, {
"id": 5,
"description": "Voluptatem commodi voluptate placeat architecto beatae illum dolores fugiat.",
"default_branch": "master",
"tag_list": [],
"public": false,
"archived": false,
"visibility_level": 0,
"ssh_url_to_repo": "phil@localhost:gitlab-org/gitlab-test.git",
"http_url_to_repo": "http://localhost:3000/gitlab-org/gitlab-test.git",
"web_url": "http://localhost:3000/gitlab-org/gitlab-test",
"name": "Gitlab Test",
"name_with_namespace": "Gitlab Org / Gitlab Test",
"path": "gitlab-test",
"path_with_namespace": "gitlab-org/gitlab-test",
"issues_enabled": true,
"merge_requests_enabled": true,
"wiki_enabled": true,
"builds_enabled": true,
"snippets_enabled": false,
"created_at": "2016-01-13T20:19:53.202Z",
"last_activity_at": "2016-01-13T20:27:41.626Z",
"shared_runners_enabled": true,
"creator_id": 1,
"namespace": {
"id": 3,
"name": "Gitlab Org",
"path": "gitlab-org",
"owner_id": null,
"created_at": "2016-01-13T20:19:48.851Z",
"updated_at": "2016-01-13T20:19:48.851Z",
"description": "Magni mollitia quod quidem soluta nesciunt impedit.",
"avatar": {
"url": null
}
},
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
"only_allow_merge_if_build_succeeds": false,
"open_issues_count": 5,
"permissions": {
"project_access": null,
"group_access": {
"access_level": 50,
"notification_level": 3
}
}
}, {
"id": 4,
"description": "Aut molestias quas est ut aperiam officia quod libero.",
"default_branch": "master",
"tag_list": [],
"public": true,
"archived": false,
"visibility_level": 20,
"ssh_url_to_repo": "phil@localhost:gitlab-org/gitlab-shell.git",
"http_url_to_repo": "http://localhost:3000/gitlab-org/gitlab-shell.git",
"web_url": "http://localhost:3000/gitlab-org/gitlab-shell",
"name": "Gitlab Shell",
"name_with_namespace": "Gitlab Org / Gitlab Shell",
"path": "gitlab-shell",
"path_with_namespace": "gitlab-org/gitlab-shell",
"issues_enabled": true,
"merge_requests_enabled": true,
"wiki_enabled": true,
"builds_enabled": true,
"snippets_enabled": false,
"created_at": "2016-01-13T20:19:51.882Z",
"last_activity_at": "2016-01-13T20:27:35.678Z",
"shared_runners_enabled": true,
"creator_id": 1,
"namespace": {
"id": 3,
"name": "Gitlab Org",
"path": "gitlab-org",
"owner_id": null,
"created_at": "2016-01-13T20:19:48.851Z",
"updated_at": "2016-01-13T20:19:48.851Z",
"description": "Magni mollitia quod quidem soluta nesciunt impedit.",
"avatar": {
"url": null
}
},
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
"only_allow_merge_if_build_succeeds": false,
"open_issues_count": 5,
"permissions": {
"project_access": {
"access_level": 20,
"notification_level": 3
},
"group_access": {
"access_level": 50,
"notification_level": 3
}
}
}, {
"id": 3,
"description": "Excepturi molestiae quia repellendus omnis est illo illum eligendi.",
"default_branch": "master",
"tag_list": [],
"public": true,
"archived": false,
"visibility_level": 20,
"ssh_url_to_repo": "phil@localhost:gitlab-org/gitlab-ci.git",
"http_url_to_repo": "http://localhost:3000/gitlab-org/gitlab-ci.git",
"web_url": "http://localhost:3000/gitlab-org/gitlab-ci",
"name": "Gitlab Ci",
"name_with_namespace": "Gitlab Org / Gitlab Ci",
"path": "gitlab-ci",
"path_with_namespace": "gitlab-org/gitlab-ci",
"issues_enabled": true,
"merge_requests_enabled": true,
"wiki_enabled": true,
"builds_enabled": true,
"snippets_enabled": false,
"created_at": "2016-01-13T20:19:50.346Z",
"last_activity_at": "2016-01-13T20:27:30.115Z",
"shared_runners_enabled": true,
"creator_id": 1,
"namespace": {
"id": 3,
"name": "Gitlab Org",
"path": "gitlab-org",
"owner_id": null,
"created_at": "2016-01-13T20:19:48.851Z",
"updated_at": "2016-01-13T20:19:48.851Z",
"description": "Magni mollitia quod quidem soluta nesciunt impedit.",
"avatar": {
"url": null
}
},
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
"only_allow_merge_if_build_succeeds": false,
"open_issues_count": 3,
"permissions": {
"project_access": null,
"group_access": {
"access_level": 50,
"notification_level": 3
}
}
}, {
"id": 2,
"description": "Adipisci quaerat dignissimos enim sed ipsam dolorem quia.",
"default_branch": "master",
"tag_list": [],
"public": false,
"archived": false,
"visibility_level": 10,
"ssh_url_to_repo": "phil@localhost:gitlab-org/gitlab-ce.git",
"http_url_to_repo": "http://localhost:3000/gitlab-org/gitlab-ce.git",
"web_url": "http://localhost:3000/gitlab-org/gitlab-ce",
"name": "Gitlab Ce",
"name_with_namespace": "Gitlab Org / Gitlab Ce",
"path": "gitlab-ce",
"path_with_namespace": "gitlab-org/gitlab-ce",
"issues_enabled": true,
"merge_requests_enabled": true,
"wiki_enabled": true,
"builds_enabled": true,
"snippets_enabled": false,
"created_at": "2016-01-13T20:19:49.065Z",
"last_activity_at": "2016-01-13T20:26:58.454Z",
"shared_runners_enabled": true,
"creator_id": 1,
"namespace": {
"id": 3,
"name": "Gitlab Org",
"path": "gitlab-org",
"owner_id": null,
"created_at": "2016-01-13T20:19:48.851Z",
"updated_at": "2016-01-13T20:19:48.851Z",
"description": "Magni mollitia quod quidem soluta nesciunt impedit.",
"avatar": {
"url": null
}
},
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
"only_allow_merge_if_build_succeeds": false,
"open_issues_count": 5,
"permissions": {
"project_access": {
"access_level": 30,
"notification_level": 3
},
"group_access": {
"access_level": 50,
"notification_level": 3
}
}
}, {
"id": 1,
"description": "Vel voluptatem maxime saepe ex quia.",
"default_branch": "master",
"tag_list": [],
"public": false,
"archived": false,
"visibility_level": 0,
"ssh_url_to_repo": "phil@localhost:documentcloud/underscore.git",
"http_url_to_repo": "http://localhost:3000/documentcloud/underscore.git",
"web_url": "http://localhost:3000/documentcloud/underscore",
"name": "Underscore",
"name_with_namespace": "Documentcloud / Underscore",
"path": "underscore",
"path_with_namespace": "documentcloud/underscore",
"issues_enabled": true,
"merge_requests_enabled": true,
"wiki_enabled": true,
"builds_enabled": true,
"snippets_enabled": false,
"created_at": "2016-01-13T20:19:45.862Z",
"last_activity_at": "2016-01-13T20:25:03.106Z",
"shared_runners_enabled": true,
"creator_id": 1,
"namespace": {
"id": 2,
"name": "Documentcloud",
"path": "documentcloud",
"owner_id": null,
"created_at": "2016-01-13T20:19:44.464Z",
"updated_at": "2016-01-13T20:19:44.464Z",
"description": "Aut impedit perferendis fuga et ipsa repellat cupiditate et.",
"avatar": {
"url": null
}
},
"avatar_url": null,
"star_count": 0,
"forks_count": 0,
"only_allow_merge_if_build_succeeds": false,
"open_issues_count": 5,
"permissions": {
"project_access": null,
"group_access": {
"access_level": 50,
"notification_level": 3
}
}
}]
require 'spec_helper'
describe ApplicationController, '(Static JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
before(:all) do
clean_frontend_fixtures('static/')
end
fixtures_path = File.expand_path(JavaScriptFixturesHelpers::FIXTURE_PATH, Rails.root)
haml_fixtures = Dir.glob(File.expand_path('**/*.haml', fixtures_path)).map do |file_path|
file_path.sub(/\A#{fixtures_path}#{File::SEPARATOR}/, '')
end
haml_fixtures.each do |template_file_name|
it "static/#{template_file_name.sub(/\.haml\z/, '.raw')}" do |example|
fixture_file_name = example.description
rendered = render_template(template_file_name)
store_frontend_fixture(rendered, fixture_file_name)
end
end
private
def render_template(template_file_name)
fixture_path = JavaScriptFixturesHelpers::FIXTURE_PATH
controller = ApplicationController.new
controller.prepend_view_path(fixture_path)
controller.render_to_string(template: template_file_name, layout: false)
end
end
require 'spec_helper'
context 'U2F' do
include JavaScriptFixturesHelpers
let(:user) { create(:user, :two_factor_via_u2f) }
before(:all) do
clean_frontend_fixtures('u2f/')
end
describe SessionsController, '(JavaScript fixtures)', type: :controller do
render_views
before do
@request.env['devise.mapping'] = Devise.mappings[:user]
end
it 'u2f/authenticate.html.raw' do |example|
allow(controller).to receive(:find_user).and_return(user)
post :create, user: { login: user.username, password: user.password }
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
end
describe Profiles::TwoFactorAuthsController, '(JavaScript fixtures)', type: :controller do
render_views
before do
sign_in(user)
end
it 'u2f/register.html.raw' do |example|
get :show
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
end
end
= render partial: "u2f/authenticate", locals: { new_user_session_path: "/users/sign_in", params: {}, resource_name: "user" }
- user = FactoryGirl.build(:user, :two_factor_via_otp)
= render partial: "u2f/register", locals: { create_u2f_profile_two_factor_auth_path: '/profile/two_factor_auth/create_u2f', current_user: user }
......@@ -43,8 +43,7 @@
}
describe('Dropdown', function describeDropdown() {
fixture.preload('gl_dropdown.html');
fixture.preload('projects.json');
preloadFixtures('static/gl_dropdown.html.raw');
function initDropDown(hasRemote, isFilterable) {
this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({
......@@ -61,10 +60,10 @@
}
beforeEach(() => {
fixture.load('gl_dropdown.html');
loadFixtures('static/gl_dropdown.html.raw');
this.dropdownContainerElement = $('.dropdown.inline');
this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
this.projectsData = fixture.load('projects.json')[0];
this.projectsData = getJSONFixture('projects.json');
});
afterEach(() => {
......
......@@ -4,11 +4,11 @@
//= require gl_field_errors
((global) => {
fixture.preload('gl_field_errors.html');
preloadFixtures('static/gl_field_errors.html.raw');
describe('GL Style Field Errors', function() {
beforeEach(function() {
fixture.load('gl_field_errors.html');
loadFixtures('static/gl_field_errors.html.raw');
const $form = this.$form = $('form.gl-show-field-errors');
this.fieldErrors = new global.GlFieldErrors($form);
});
......
......@@ -7,7 +7,7 @@
describe('Header', function() {
var todosPendingCount = '.todos-pending-count';
var fixtureTemplate = 'header.html';
var fixtureTemplate = 'static/header.html.raw';
function isTodosCountHidden() {
return $(todosPendingCount).hasClass('hidden');
......@@ -17,9 +17,9 @@
$(document).trigger('todo:toggle', newCount);
}
fixture.preload(fixtureTemplate);
preloadFixtures(fixtureTemplate);
beforeEach(function() {
fixture.load(fixtureTemplate);
loadFixtures(fixtureTemplate);
});
it('should update todos-pending-count after receiving the todo:toggle event', function() {
......
......@@ -21,10 +21,10 @@
}
describe('Issuable', () => {
fixture.preload('issuable_filter');
preloadFixtures('static/issuable_filter.html.raw');
beforeEach(() => {
fixture.load('issuable_filter');
loadFixtures('static/issuable_filter.html.raw');
Issuable.init();
});
......@@ -37,7 +37,7 @@
beforeEach(() => {
$filtersForm = $('.js-filter-form');
fixture.load('issuable_filter');
loadFixtures('static/issuable_filter.html.raw');
resetForm($filtersForm);
});
......
......@@ -8,9 +8,9 @@
var INVALID_URL = 'http://goesnowhere.nothing/whereami';
var $boxClosed, $boxOpen, $btnClose, $btnReopen;
fixture.preload('issues/closed-issue.html');
fixture.preload('issues/issue-with-task-list.html');
fixture.preload('issues/open-issue.html');
preloadFixtures('issues/closed-issue.html.raw');
preloadFixtures('issues/issue-with-task-list.html.raw');
preloadFixtures('issues/open-issue.html.raw');
function expectErrorMessage() {
var $flashMessage = $('div.flash-alert');
......@@ -61,8 +61,8 @@
describe('Issue', function() {
describe('task lists', function() {
fixture.load('issues/issue-with-task-list.html');
beforeEach(function() {
loadFixtures('issues/issue-with-task-list.html.raw');
this.issue = new Issue();
});
......@@ -86,7 +86,7 @@
describe('close issue', function() {
beforeEach(function() {
fixture.load('issues/open-issue.html');
loadFixtures('issues/open-issue.html.raw');
findElements();
this.issue = new Issue();
......@@ -140,7 +140,7 @@
describe('reopen issue', function() {
beforeEach(function() {
fixture.load('issues/closed-issue.html');
loadFixtures('issues/closed-issue.html.raw');
findElements();
this.issue = new Issue();
......
......@@ -17,10 +17,10 @@
(() => {
let saveLabelCount = 0;
describe('Issue dropdown sidebar', () => {
fixture.preload('issue_sidebar_label.html');
preloadFixtures('static/issue_sidebar_label.html.raw');
beforeEach(() => {
fixture.load('issue_sidebar_label.html');
loadFixtures('static/issue_sidebar_label.html.raw');
new IssuableContext('{"id":1,"name":"Administrator","username":"root"}');
new LabelsSelect();
......
......@@ -6,7 +6,7 @@
(function() {
describe('LineHighlighter', function() {
var clickLine;
fixture.preload('line_highlighter.html');
preloadFixtures('static/line_highlighter.html.raw');
clickLine = function(number, eventData) {
var e;
if (eventData == null) {
......@@ -20,7 +20,7 @@
}
};
beforeEach(function() {
fixture.load('line_highlighter.html');
loadFixtures('static/line_highlighter.html.raw');
this["class"] = new LineHighlighter();
this.css = this["class"].highlightClass;
return this.spies = {
......
......@@ -6,9 +6,9 @@
(function() {
describe('MergeRequest', function() {
return describe('task lists', function() {
fixture.preload('merge_requests_show.html');
preloadFixtures('static/merge_requests_show.html.raw');
beforeEach(function() {
fixture.load('merge_requests_show.html');
loadFixtures('static/merge_requests_show.html.raw');
return this.merge = new MergeRequest();
});
it('modifies the Markdown field', function() {
......
......@@ -16,7 +16,7 @@
};
$.extend(stubLocation, defaults, stubs || {});
};
fixture.preload('merge_request_tabs.html');
preloadFixtures('static/merge_request_tabs.html.raw');
beforeEach(function () {
this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation });
......@@ -30,7 +30,7 @@
describe('#activateTab', function () {
beforeEach(function () {
spyOn($, 'ajax').and.callFake(function () {});
fixture.load('merge_request_tabs.html');
loadFixtures('static/merge_request_tabs.html.raw');
this.subject = this.class.activateTab;
});
it('shows the first tab when action is show', function () {
......
......@@ -5,10 +5,10 @@
(() => {
describe('Mini Pipeline Graph Dropdown', () => {
fixture.preload('mini_dropdown_graph');
preloadFixtures('static/mini_dropdown_graph.html.raw');
beforeEach(() => {
fixture.load('mini_dropdown_graph');
loadFixtures('static/mini_dropdown_graph.html.raw');
});
describe('When is initialized', () => {
......
......@@ -8,7 +8,7 @@
describe('Branch', function() {
return describe('create a new branch', function() {
var expectToHaveError, fillNameWith;
fixture.preload('new_branch.html');
preloadFixtures('static/new_branch.html.raw');
fillNameWith = function(value) {
return $('.js-branch-name').val(value).trigger('blur');
};
......@@ -16,7 +16,7 @@
return expect($('.js-branch-name-error span').text()).toEqual(error);
};
beforeEach(function() {
fixture.load('new_branch.html');
loadFixtures('static/new_branch.html.raw');
$('form').on('submit', function(e) {
return e.preventDefault();
});
......
......@@ -12,11 +12,11 @@
gl.utils = gl.utils || {};
describe('Notes', function() {
var commentsTemplate = 'issues/issue_with_comment.raw';
fixture.preload(commentsTemplate);
var commentsTemplate = 'issues/issue_with_comment.html.raw';
preloadFixtures(commentsTemplate);
beforeEach(function () {
fixture.load(commentsTemplate);
loadFixtures(commentsTemplate);
gl.utils.disableButtonIfEmptyField = _.noop;
window.project_uploads_path = 'http://test.host/uploads';
$('body').data('page', 'projects:issues:show');
......
......@@ -2,10 +2,10 @@
(() => {
describe('Pipelines', () => {
fixture.preload('pipeline_graph');
preloadFixtures('static/pipeline_graph.html.raw');
beforeEach(() => {
fixture.load('pipeline_graph');
loadFixtures('static/pipeline_graph.html.raw');
});
it('should be defined', () => {
......
......@@ -16,10 +16,9 @@
window.gon.api_version = 'v3';
describe('Project Title', function() {
fixture.preload('project_title.html');
fixture.preload('projects.json');
preloadFixtures('static/project_title.html.raw');
beforeEach(function() {
fixture.load('project_title.html');
loadFixtures('static/project_title.html.raw');
return this.project = new Project();
});
return describe('project list', function() {
......@@ -34,7 +33,7 @@
beforeEach((function(_this) {
return function() {
_this.projects_data = fixture.load('projects.json')[0];
_this.projects_data = getJSONFixture('projects.json');
return spyOn(jQuery, 'ajax').and.callFake(fakeAjaxResponse.bind(_this));
};
})(this));
......
......@@ -36,9 +36,9 @@
describe('RightSidebar', function() {
var fixtureName = 'issues/open-issue.html.raw';
fixture.preload(fixtureName);
preloadFixtures(fixtureName);
beforeEach(function() {
fixture.load(fixtureName);
loadFixtures(fixtureName);
this.sidebar = new Sidebar;
$aside = $('.right-sidebar');
$page = $('.page-with-sidebar');
......@@ -65,9 +65,10 @@
});
it('should broadcast todo:toggle event when add todo clicked', function() {
var todos = getJSONFixture('todos.json');
spyOn(jQuery, 'ajax').and.callFake(function() {
var d = $.Deferred();
var response = fixture.load('todos.json');
var response = todos;
d.resolve(response);
return d.promise();
});
......
......@@ -112,9 +112,9 @@
};
describe('Search autocomplete dropdown', function() {
fixture.preload('search_autocomplete.html');
preloadFixtures('static/search_autocomplete.html.raw');
beforeEach(function() {
fixture.load('search_autocomplete.html');
loadFixtures('static/search_autocomplete.html.raw');
return widget = new gl.SearchAutocomplete;
});
it('should show Dashboard specific dropdown menu', function() {
......
......@@ -6,9 +6,9 @@
(function() {
describe('ShortcutsIssuable', function() {
var fixtureName = 'issues/open-issue.html.raw';
fixture.preload(fixtureName);
preloadFixtures(fixtureName);
beforeEach(function() {
fixture.load(fixtureName);
loadFixtures(fixtureName);
document.querySelector('.js-new-note-form').classList.add('js-main-target-form');
return this.shortcut = new ShortcutsIssuable();
});
......
......@@ -2,7 +2,7 @@
((global) => {
describe('SigninTabsMemoizer', () => {
const fixtureTemplate = 'signin_tabs.html';
const fixtureTemplate = 'static/signin_tabs.html.raw';
const tabSelector = 'ul.nav-tabs';
const currentTabKey = 'current_signin_tab';
let memo;
......@@ -15,10 +15,10 @@
return memo;
}
fixture.preload(fixtureTemplate);
preloadFixtures(fixtureTemplate);
beforeEach(() => {
fixture.load(fixtureTemplate);
loadFixtures(fixtureTemplate);
});
it('does nothing if no tab was previously selected', () => {
......
......@@ -103,7 +103,7 @@
describe('DOM Events', function () {
beforeEach(function () {
// This ensures DOM and DOM events are initialized for these specs.
fixture.set('<div></div>');
setFixtures('<div></div>');
this.smartInterval = createDefaultSmartInterval();
});
......
......@@ -37,12 +37,12 @@
// file as a manifest.
// For more information: http://github.com/modeset/teaspoon
(function() {
}).call(this);
// set our fixtures path
jasmine.getFixtures().fixturesPath = '/teaspoon/fixtures';
jasmine.getJSONFixtures().fixturesPath = '/teaspoon/fixtures';
// defined in ActionDispatch::TestRequest
// see https://github.com/rails/rails/blob/v4.2.7.1/actionpack/lib/action_dispatch/testing/test_request.rb#L7
window.gl = window.gl || {};
gl.TEST_HOST = 'http://test.host';
window.gl.TEST_HOST = 'http://test.host';
window.gon = window.gon || {};
......@@ -13,7 +13,7 @@
};
describe('on a js-syntax-highlight element', function() {
beforeEach(function() {
return fixture.set('<div class="js-syntax-highlight"></div>');
return setFixtures('<div class="js-syntax-highlight"></div>');
});
return it('applies syntax highlighting', function() {
stubUserColorScheme('monokai');
......@@ -23,7 +23,7 @@
});
return describe('on a parent element', function() {
beforeEach(function() {
return fixture.set("<div class=\"parent\">\n <div class=\"js-syntax-highlight\"></div>\n <div class=\"foo\"></div>\n <div class=\"js-syntax-highlight\"></div>\n</div>");
return setFixtures("<div class=\"parent\">\n <div class=\"js-syntax-highlight\"></div>\n <div class=\"foo\"></div>\n <div class=\"js-syntax-highlight\"></div>\n</div>");
});
it('applies highlighting to all applicable children', function() {
stubUserColorScheme('monokai');
......@@ -33,7 +33,7 @@
});
return it('prevents an infinite loop when no matches exist', function() {
var highlight;
fixture.set('<div></div>');
setFixtures('<div></div>');
highlight = function() {
return $('div').syntaxHighlight();
};
......
......@@ -10,8 +10,10 @@
(function() {
describe('U2FAuthenticate', function() {
fixture.load('u2f/authenticate');
preloadFixtures('u2f/authenticate.html.raw');
beforeEach(function() {
loadFixtures('u2f/authenticate.html.raw');
this.u2fDevice = new MockU2FDevice;
this.container = $("#js-authenticate-u2f");
this.component = new window.gl.U2FAuthenticate(
......
......@@ -10,8 +10,10 @@
(function() {
describe('U2FRegister', function() {
fixture.load('u2f/register');
preloadFixtures('u2f/register.html.raw');
beforeEach(function() {
loadFixtures('u2f/register.html.raw');
this.u2fDevice = new MockU2FDevice;
this.container = $("#js-register-u2f");
this.component = new U2FRegister(this.container, $("#js-register-u2f-templates"), {}, "token");
......
......@@ -5,7 +5,7 @@ describe('Commit component', () => {
let component;
it('should render a code-fork icon if it does not represent a tag', () => {
fixture.set('<div class="test-commit-container"></div>');
setFixtures('<div class="test-commit-container"></div>');
component = new window.gl.CommitComponent({
el: document.querySelector('.test-commit-container'),
propsData: {
......@@ -30,7 +30,7 @@ describe('Commit component', () => {
describe('Given all the props', () => {
beforeEach(() => {
fixture.set('<div class="test-commit-container"></div>');
setFixtures('<div class="test-commit-container"></div>');
props = {
tag: true,
......@@ -105,7 +105,7 @@ describe('Commit component', () => {
describe('When commit title is not provided', () => {
it('should render default message', () => {
fixture.set('<div class="test-commit-container"></div>');
setFixtures('<div class="test-commit-container"></div>');
props = {
tag: false,
commitRef: {
......
......@@ -10,9 +10,9 @@
describe('ZenMode', function() {
var fixtureName = 'issues/open-issue.html.raw';
fixture.preload(fixtureName);
preloadFixtures(fixtureName);
beforeEach(function() {
fixture.load(fixtureName);
loadFixtures(fixtureName);
spyOn(Dropzone, 'forElement').and.callFake(function() {
return {
enable: function() {
......
......@@ -136,6 +136,14 @@ describe Ci::Ansi2html, lib: true do
expect(subject.convert("<")[:html]).to eq('&lt;')
end
it "replaces newlines with line break tags" do
expect(subject.convert("\n")[:html]).to eq('<br>')
end
it "groups carriage returns with newlines" do
expect(subject.convert("\r\n")[:html]).to eq('<br>')
end
describe "incremental update" do
shared_examples 'stateable converter' do
let(:pass1) { subject.convert(pre_text) }
......
......@@ -248,6 +248,7 @@ DeployKey:
- fingerprint
- public
- can_push
- last_used_at
Service:
- id
- type
......
......@@ -15,9 +15,9 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey }
it 'should block user in GitLab' do
expect(access).to receive(:block_user).with(user, 'does not exist anymore')
access.allowed?
expect(user).to be_blocked
expect(user).to be_ldap_blocked
end
end
......@@ -34,9 +34,9 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey }
it 'blocks user in GitLab' do
expect(access).to receive(:block_user).with(user, 'is disabled in Active Directory')
access.allowed?
expect(user).to be_blocked
expect(user).to be_ldap_blocked
end
end
......@@ -53,7 +53,10 @@ describe Gitlab::LDAP::Access, lib: true do
end
it 'does not unblock user in GitLab' do
expect(access).not_to receive(:unblock_user)
access.allowed?
expect(user).to be_blocked
expect(user).not_to be_ldap_blocked # this block is handled by omniauth not by our internal logic
end
......@@ -65,8 +68,9 @@ describe Gitlab::LDAP::Access, lib: true do
end
it 'unblocks user in GitLab' do
expect(access).to receive(:unblock_user).with(user, 'is not disabled anymore')
access.allowed?
expect(user).not_to be_blocked
end
end
end
......@@ -87,9 +91,9 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey }
it 'blocks user in GitLab' do
expect(access).to receive(:block_user).with(user, 'does not exist anymore')
access.allowed?
expect(user).to be_blocked
expect(user).to be_ldap_blocked
end
end
......@@ -99,11 +103,54 @@ describe Gitlab::LDAP::Access, lib: true do
end
it 'unblocks the user if it exists' do
expect(access).to receive(:unblock_user).with(user, 'is available again')
access.allowed?
expect(user).not_to be_blocked
end
end
end
end
end
describe '#block_user' do
before do
user.activate
allow(Gitlab::AppLogger).to receive(:info)
access.block_user user, 'reason'
end
it 'blocks the user' do
expect(user).to be_blocked
expect(user).to be_ldap_blocked
end
it 'logs the reason' do
expect(Gitlab::AppLogger).to have_received(:info).with(
"LDAP account \"123456\" reason, " +
"blocking Gitlab user \"#{user.name}\" (#{user.email})"
)
end
end
describe '#unblock_user' do
before do
user.ldap_block
allow(Gitlab::AppLogger).to receive(:info)
access.unblock_user user, 'reason'
end
it 'activates the user' do
expect(user).not_to be_blocked
expect(user).not_to be_ldap_blocked
end
it 'logs the reason' do
Gitlab::AppLogger.info(
"LDAP account \"123456\" reason, " +
"unblocking Gitlab user \"#{user.name}\" (#{user.email})"
)
end
end
end
......@@ -129,4 +129,27 @@ describe Gitlab::LDAP::Config, lib: true do
expect(config.has_auth?).to be_falsey
end
end
describe '#attributes' do
it 'uses default attributes when no custom attributes are configured' do
expect(config.attributes).to eq(config.default_attributes)
end
it 'merges the configuration attributes with default attributes' do
stub_ldap_config(
options: {
'attributes' => {
'username' => %w(sAMAccountName),
'email' => %w(userPrincipalName)
}
}
)
expect(config.attributes).to include({
'username' => %w(sAMAccountName),
'email' => %w(userPrincipalName),
'name' => 'cn'
})
end
end
end
......@@ -7,9 +7,11 @@ describe Gitlab::LDAP::Person do
before do
stub_ldap_config(
attributes: {
name: 'cn',
email: %w(mail email userPrincipalName)
options: {
'attributes' => {
'name' => 'cn',
'email' => %w(mail email userPrincipalName)
}
}
)
end
......@@ -30,7 +32,7 @@ describe Gitlab::LDAP::Person do
entry['mail'] = mail
person = Gitlab::LDAP::Person.new(entry, 'ldapmain')
expect(person.email).to eq(mail)
expect(person.email).to eq([mail])
end
it 'returns the value of userPrincipalName, if mail and email are not present' do
......@@ -38,7 +40,7 @@ describe Gitlab::LDAP::Person do
entry['userPrincipalName'] = user_principal_name
person = Gitlab::LDAP::Person.new(entry, 'ldapmain')
expect(person.email).to eq(user_principal_name)
expect(person.email).to eq([user_principal_name])
end
end
end
......@@ -7,26 +7,72 @@ describe GlobalMilestone, models: true do
let(:project1) { create(:project, group: group) }
let(:project2) { create(:project, path: 'gitlab-ci', group: group) }
let(:project3) { create(:project, path: 'cookbook-gitlab', group: group) }
let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) }
let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) }
let(:milestone1_project3) { create(:milestone, title: "Milestone v1.2", project: project3) }
let(:milestone2_project1) { create(:milestone, title: "VD-123", project: project1) }
let(:milestone2_project2) { create(:milestone, title: "VD-123", project: project2) }
let(:milestone2_project3) { create(:milestone, title: "VD-123", project: project3) }
describe '.build_collection' do
let(:milestone1_due_date) { 2.weeks.from_now.to_date }
let!(:milestone1_project1) do
create(
:milestone,
title: "Milestone v1.2",
project: project1,
due_date: milestone1_due_date
)
end
let!(:milestone1_project2) do
create(
:milestone,
title: "Milestone v1.2",
project: project2,
due_date: milestone1_due_date
)
end
let!(:milestone1_project3) do
create(
:milestone,
title: "Milestone v1.2",
project: project3,
due_date: milestone1_due_date
)
end
let!(:milestone2_project1) do
create(
:milestone,
title: "VD-123",
project: project1,
due_date: nil
)
end
let!(:milestone2_project2) do
create(
:milestone,
title: "VD-123",
project: project2,
due_date: nil
)
end
let!(:milestone2_project3) do
create(
:milestone,
title: "VD-123",
project: project3,
due_date: nil
)
end
before do
milestones =
[
milestone1_project1,
milestone1_project2,
milestone1_project3,
milestone2_project1,
milestone2_project2,
milestone2_project3
projects = [
project1,
project2,
project3
]
@global_milestones = GlobalMilestone.build_collection(milestones)
@global_milestones = GlobalMilestone.build_collection(projects, {})
end
it 'has all project milestones' do
......@@ -40,9 +86,17 @@ describe GlobalMilestone, models: true do
it 'has all project milestones' do
expect(@global_milestones.map { |group_milestone| group_milestone.milestones.count }.sum).to eq(6)
end
it 'sorts collection by due date' do
expect(@global_milestones.map(&:due_date)).to eq [nil, milestone1_due_date]
end
end
describe '#initialize' do
let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) }
let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) }
let(:milestone1_project3) { create(:milestone, title: "Milestone v1.2", project: project3) }
before do
milestones =
[
......
require 'spec_helper'
describe GroupMilestone, models: true do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
let(:project_milestone) do
create(:milestone, title: "Milestone v1.2", project: project)
end
describe '.build' do
it 'returns milestone with group assigned' do
milestone = GroupMilestone.build(
group,
[project],
project_milestone.title
)
expect(milestone.group).to eq group
end
end
describe '.build_collection' do
before do
project_milestone
end
it 'returns array of milestones, each with group assigned' do
milestones = GroupMilestone.build_collection(group, [project], {})
expect(milestones).to all(have_attributes(group: group))
end
end
end
......@@ -28,6 +28,15 @@ describe Key, models: true do
expect(build(:key, user: user).publishable_key).to include("#{user.name} (#{Gitlab.config.gitlab.host})")
end
end
describe "#update_last_used_at" do
it "enqueues a UseKeyWorker job" do
key = create(:key)
expect(UseKeyWorker).to receive(:perform_async).with(key.id)
key.update_last_used_at
end
end
end
context "validation of uniqueness (based on fingerprint uniqueness)" do
......
......@@ -265,6 +265,14 @@ describe API::Users, api: true do
expect(response).to have_http_status(409)
expect(json_response['message']).to eq('Username has already been taken')
end
it 'creates user with new identity' do
post api("/users", admin), attributes_for(:user, provider: 'github', extern_uid: '67890')
expect(response).to have_http_status(201)
expect(json_response['identities'].first['extern_uid']).to eq('67890')
expect(json_response['identities'].first['provider']).to eq('github')
end
end
end
......
require 'spec_helper'
describe Projects::ParticipantsService, services: true do
describe '#groups' do
describe 'avatar_url' do
let(:project) { create(:empty_project, :public) }
let(:group) { create(:group, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/dk.png')) }
let(:user) { create(:user) }
let(:base_url) { Settings.send(:build_base_gitlab_url) }
let!(:group_member) { create(:group_member, group: group, user: user) }
it 'should return an url for the avatar' do
participants = described_class.new(project, user)
groups = participants.groups
expect(groups.size).to eq 1
expect(groups.first[:avatar_url]).to eq "#{base_url}/uploads/group/avatar/#{group.id}/dk.png"
end
it 'should return an url for the avatar with relative url' do
stub_config_setting(relative_url_root: '/gitlab')
stub_config_setting(url: Settings.send(:build_gitlab_url))
participants = described_class.new(project, user)
groups = participants.groups
expect(groups.size).to eq 1
expect(groups.first[:avatar_url]).to eq "#{base_url}/gitlab/uploads/group/avatar/#{group.id}/dk.png"
end
end
end
end
......@@ -20,12 +20,26 @@ module JavaScriptFixturesHelpers
# Public: Store a response object as fixture file
#
# response - response object to store
# response - string or response object to store
# fixture_file_name - file name to store the fixture in (relative to FIXTURE_PATH)
#
def store_frontend_fixture(response, fixture_file_name)
fixture_file_name = File.expand_path(fixture_file_name, FIXTURE_PATH)
fixture = response.respond_to?(:body) ? parse_response(response) : response
FileUtils.mkdir_p(File.dirname(fixture_file_name))
File.write(fixture_file_name, fixture)
end
private
# Private: Prepare a response object for use as a frontend fixture
#
# response - response object to prepare
#
def parse_response(response)
fixture = response.body
fixture.force_encoding("utf-8")
response_mime_type = Mime::Type.lookup(response.content_type)
if response_mime_type.html?
......@@ -34,7 +48,7 @@ module JavaScriptFixturesHelpers
link_tags = doc.css('link')
link_tags.remove
scripts = doc.css('script')
scripts = doc.css("script:not([type='text/template'])")
scripts.remove
fixture = doc.to_html
......@@ -44,7 +58,6 @@ module JavaScriptFixturesHelpers
fixture.gsub!(%r{="/}, "=\"http://#{test_host}/")
end
FileUtils.mkdir_p(File.dirname(fixture_file_name))
File.write(fixture_file_name, fixture)
fixture
end
end
......@@ -7,6 +7,7 @@ describe 'projects/merge_requests/show.html.haml' do
let(:project) { create(:project) }
let(:fork_project) { create(:project, forked_from_project: project) }
let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
let(:note) { create(:note_on_merge_request, project: project, noteable: closed_merge_request) }
let(:closed_merge_request) do
create(:closed_merge_request,
......@@ -19,8 +20,12 @@ describe 'projects/merge_requests/show.html.haml' do
assign(:project, project)
assign(:merge_request, closed_merge_request)
assign(:commits_count, 0)
assign(:note, note)
assign(:noteable, closed_merge_request)
assign(:notes, [])
assign(:pipelines, Ci::Pipeline.none)
allow(view).to receive(:can?).and_return(true)
allow(view).to receive_messages(current_user: user, can?: true)
end
context 'when the merge request is closed' do
......
require 'spec_helper'
describe UseKeyWorker do
describe "#perform" do
it "updates the key's last_used_at attribute to the current time when it exists" do
worker = described_class.new
key = create(:key)
current_time = Time.zone.now
Timecop.freeze(current_time) do
expect { worker.perform(key.id) }
.to change { key.reload.last_used_at }.from(nil).to be_like_time(current_time)
end
end
it "returns false and skips the job when the key doesn't exist" do
worker = described_class.new
key = create(:key)
expect(worker.perform(key.id + 1)).to eq false
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