Commit 6625f479 authored by Regis's avatar Regis

Merge branch 'master' into auto-pipelines-vue

parents 55df5536 6c624821
...@@ -2,6 +2,26 @@ ...@@ -2,6 +2,26 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. 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) ## 8.15.2 (2016-12-27)
- Fix finding the latest pipeline. !8301 - Fix finding the latest pipeline. !8301
......
...@@ -84,10 +84,14 @@ gem 'dropzonejs-rails', '~> 0.7.1' ...@@ -84,10 +84,14 @@ gem 'dropzonejs-rails', '~> 0.7.1'
# for backups # for backups
gem 'fog-aws', '~> 0.9' gem 'fog-aws', '~> 0.9'
gem 'fog-core', '~> 1.40' gem 'fog-core', '~> 1.40'
gem 'fog-google', '~> 0.5'
gem 'fog-local', '~> 0.3' gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1' gem 'fog-openstack', '~> 0.1'
gem 'fog-rackspace', '~> 0.1.1' gem 'fog-rackspace', '~> 0.1.1'
# for Google storage
gem 'google-api-client', '~> 0.8.6'
# for aws storage # for aws storage
gem 'unf', '~> 0.1.4' gem 'unf', '~> 0.1.4'
......
...@@ -58,6 +58,10 @@ GEM ...@@ -58,6 +58,10 @@ GEM
attr_encrypted (3.0.3) attr_encrypted (3.0.3)
encryptor (~> 3.0.0) encryptor (~> 3.0.0)
attr_required (1.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) autoprefixer-rails (6.2.3)
execjs execjs
json json
...@@ -179,6 +183,7 @@ GEM ...@@ -179,6 +183,7 @@ GEM
excon (0.52.0) excon (0.52.0)
execjs (2.6.0) execjs (2.6.0)
expression_parser (0.9.0) expression_parser (0.9.0)
extlib (0.9.16)
factory_girl (4.7.0) factory_girl (4.7.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
factory_girl_rails (4.7.0) factory_girl_rails (4.7.0)
...@@ -208,6 +213,10 @@ GEM ...@@ -208,6 +213,10 @@ GEM
builder builder
excon (~> 0.49) excon (~> 0.49)
formatador (~> 0.2) formatador (~> 0.2)
fog-google (0.5.0)
fog-core
fog-json
fog-xml
fog-json (1.0.2) fog-json (1.0.2)
fog-core (~> 1.0) fog-core (~> 1.0)
multi_json (~> 1.10) multi_json (~> 1.10)
...@@ -279,6 +288,25 @@ GEM ...@@ -279,6 +288,25 @@ GEM
json json
multi_json multi_json
request_store (>= 1.0) 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) grape (0.18.0)
activesupport activesupport
builder builder
...@@ -381,11 +409,16 @@ GEM ...@@ -381,11 +409,16 @@ GEM
listen (3.0.5) listen (3.0.5)
rb-fsevent (>= 0.9.3) rb-fsevent (>= 0.9.3)
rb-inotify (>= 0.9) 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) loofah (2.0.3)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.6.4) mail (2.6.4)
mime-types (>= 1.16, < 4) mime-types (>= 1.16, < 4)
mail_room (0.9.0) mail_room (0.9.0)
memoist (0.15.0)
method_source (0.8.2) method_source (0.8.2)
mime-types (2.99.3) mime-types (2.99.3)
mimemagic (0.3.0) mimemagic (0.3.0)
...@@ -473,6 +506,7 @@ GEM ...@@ -473,6 +506,7 @@ GEM
org-ruby (0.9.12) org-ruby (0.9.12)
rubypants (~> 0.2) rubypants (~> 0.2)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (0.9.6)
paranoia (2.2.0) paranoia (2.2.0)
activerecord (>= 4.0, < 5.1) activerecord (>= 4.0, < 5.1)
parser (2.3.1.4) parser (2.3.1.4)
...@@ -584,6 +618,7 @@ GEM ...@@ -584,6 +618,7 @@ GEM
http-cookie (>= 1.0.2, < 2.0) http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0) mime-types (>= 1.16, < 4.0)
netrc (~> 0.8) netrc (~> 0.8)
retriable (1.4.1)
rinku (2.0.0) rinku (2.0.0)
rotp (2.1.2) rotp (2.1.2)
rouge (2.0.7) rouge (2.0.7)
...@@ -675,6 +710,11 @@ GEM ...@@ -675,6 +710,11 @@ GEM
sidekiq (>= 4.2.1) sidekiq (>= 4.2.1)
sidekiq-limit_fetch (3.4.0) sidekiq-limit_fetch (3.4.0)
sidekiq (>= 4) sidekiq (>= 4)
signet (0.7.3)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (~> 1.5)
multi_json (~> 1.10)
simplecov (0.12.0) simplecov (0.12.0)
docile (~> 1.1.0) docile (~> 1.1.0)
json (>= 1.8, < 3) json (>= 1.8, < 3)
...@@ -841,6 +881,7 @@ DEPENDENCIES ...@@ -841,6 +881,7 @@ DEPENDENCIES
flay (~> 2.6.1) flay (~> 2.6.1)
fog-aws (~> 0.9) fog-aws (~> 0.9)
fog-core (~> 1.40) fog-core (~> 1.40)
fog-google (~> 0.5)
fog-local (~> 0.3) fog-local (~> 0.3)
fog-openstack (~> 0.1) fog-openstack (~> 0.1)
fog-rackspace (~> 0.1.1) fog-rackspace (~> 0.1.1)
...@@ -856,6 +897,7 @@ DEPENDENCIES ...@@ -856,6 +897,7 @@ DEPENDENCIES
gollum-lib (~> 4.2) gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2) gollum-rugged_adapter (~> 0.4.2)
gon (~> 6.1.0) gon (~> 6.1.0)
google-api-client (~> 0.8.6)
grape (~> 0.18.0) grape (~> 0.18.0)
grape-entity (~> 0.6.0) grape-entity (~> 0.6.0)
haml_lint (~> 0.18.2) haml_lint (~> 0.18.2)
......
...@@ -45,14 +45,28 @@ ...@@ -45,14 +45,28 @@
const issue = this.list.findIssue(this.detailIssue.issue.id); const issue = this.list.findIssue(this.detailIssue.issue.id);
if (issue) { if (issue) {
const offsetLeft = this.$el.offsetLeft;
const boardsList = document.querySelectorAll('.boards-list')[0]; const boardsList = document.querySelectorAll('.boards-list')[0];
const right = (this.$el.offsetLeft + this.$el.offsetWidth) - boardsList.offsetWidth; const left = boardsList.scrollLeft - offsetLeft;
const left = boardsList.scrollLeft - this.$el.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) { if (right - boardsList.scrollLeft > 0) {
boardsList.scrollLeft = right; $(boardsList).animate({
scrollLeft: right
}, this.sortableOptions.animation);
} else if (left > 0) { } else if (left > 0) {
boardsList.scrollLeft = this.$el.offsetLeft; $(boardsList).animate({
scrollLeft: offsetLeft
}, this.sortableOptions.animation);
} }
} }
}, },
...@@ -65,7 +79,7 @@ ...@@ -65,7 +79,7 @@
} }
}, },
mounted () { mounted () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({ this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled, disabled: this.disabled,
group: 'boards', group: 'boards',
draggable: '.is-draggable', draggable: '.is-draggable',
...@@ -84,7 +98,7 @@ ...@@ -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 @@ ...@@ -20,6 +20,7 @@
gl.issueBoards.getBoardSortableDefaultOptions = (obj) => { gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
let defaultSortOptions = { let defaultSortOptions = {
animation: 200,
forceFallback: true, forceFallback: true,
fallbackClass: 'is-dragging', fallbackClass: 'is-dragging',
fallbackOnBody: true, fallbackOnBody: true,
......
...@@ -210,7 +210,9 @@ ...@@ -210,7 +210,9 @@
new gl.Members(); new gl.Members();
new UsersSelect(); new UsersSelect();
break; 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.MemberExpirationDate();
new gl.Members(); new gl.Members();
new UsersSelect(); new UsersSelect();
...@@ -256,10 +258,6 @@ ...@@ -256,10 +258,6 @@
case 'projects:artifacts:browse': case 'projects:artifacts:browse':
new BuildArtifacts(); new BuildArtifacts();
break; break;
case 'projects:group_links:index':
new gl.MemberExpirationDate();
new GroupsSelect();
break;
case 'search:show': case 'search:show':
new Search(); new Search();
break; break;
......
...@@ -80,9 +80,12 @@ ...@@ -80,9 +80,12 @@
} }
parseSelectedDate() { parseSelectedDate() {
this.rawSelectedDate = $("input[name='" + this.fieldName + "']").val(); this.rawSelectedDate = $(`input[name='${this.fieldName}']`).val();
if (this.rawSelectedDate.length) { 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); this.displayedDate = $.datepicker.formatDate('M d, yy', dateObj);
} else { } else {
this.displayedDate = 'No due date'; this.displayedDate = 'No due date';
......
...@@ -216,7 +216,7 @@ ...@@ -216,7 +216,7 @@
<th class="environments-deploy">Last deployment</th> <th class="environments-deploy">Last deployment</th>
<th class="environments-build">Build</th> <th class="environments-build">Build</th>
<th class="environments-commit">Commit</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> <th class="hidden-xs environments-actions"></th>
</tr> </tr>
</thead> </thead>
......
...@@ -44,9 +44,25 @@ ...@@ -44,9 +44,25 @@
} }
}; };
gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) { 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'); selectedSplit = selected.split('\n');
startChar = !wrap && textArea.selectionStart > 0 ? '\n' : ''; startChar = !wrap && textArea.selectionStart > 0 ? '\n' : '';
if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) { if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) {
if (blockTag != null) { if (blockTag != null) {
insertText = this.blockTagText(text, textArea, blockTag, selected); insertText = this.blockTagText(text, textArea, blockTag, selected);
...@@ -62,6 +78,15 @@ ...@@ -62,6 +78,15 @@
} else { } else {
insertText = "" + startChar + tag + selected + (wrap ? tag : ' '); insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
} }
if (removedFirstNewLine) {
insertText = '\n' + insertText;
}
if (removedLastNewLine) {
insertText += '\n';
}
if (document.queryCommandSupported('insertText')) { if (document.queryCommandSupported('insertText')) {
inserted = document.execCommand('insertText', false, insertText); inserted = document.execCommand('insertText', false, insertText);
} }
...@@ -74,9 +99,9 @@ ...@@ -74,9 +99,9 @@
document.execCommand("ms-endUndoUnit"); document.execCommand("ms-endUndoUnit");
} catch (error) {} } 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; var pos;
if (!textArea.setSelectionRange) { if (!textArea.setSelectionRange) {
return; return;
...@@ -87,6 +112,11 @@ ...@@ -87,6 +112,11 @@
} else { } else {
pos = textArea.selectionStart; pos = textArea.selectionStart;
} }
if (removedLastNewLine) {
pos -= 1;
}
return textArea.setSelectionRange(pos, pos); 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 // 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 // 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 // `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. // 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() { function toggleClearInput() {
$(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== ''); $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
} }
const inputs = $(selector);
var inputs = $('.js-access-expiration-date');
inputs.datepicker({ inputs.datepicker({
dateFormat: 'yy-mm-dd', dateFormat: 'yy-mm-dd',
minDate: 1, minDate: 1,
onSelect: function () { onSelect: function onSelect() {
$(this).trigger('change'); $(this).trigger('change');
toggleClearInput.call(this); toggleClearInput.call(this);
} },
}); });
inputs.next('.js-clear-input').on('click', function(event) { inputs.next('.js-clear-input').on('click', function clicked(event) {
event.preventDefault(); 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) input.datepicker('setDate', null)
.trigger('change'); .trigger('change');
toggleClearInput.call(input); toggleClearInput.call(input);
......
...@@ -895,7 +895,9 @@ ...@@ -895,7 +895,9 @@
new GLForm($editForm.find('form')); 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-id').val(targetId);
$editForm.find('.js-form-target-type').val(targetType); $editForm.find('.js-form-target-type').val(targetType);
$editForm.find('.js-note-text').focus().val(originalContent); $editForm.find('.js-note-text').focus().val(originalContent);
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
}); });
$('.no-template', this.dropdown.parent()).on('click', () => { $('.no-template', this.dropdown.parent()).on('click', () => {
this.currentTemplate = ''; this.currentTemplate.content = '';
this.setInputValueToTemplateContent(); this.setInputValueToTemplateContent();
$('.dropdown-toggle-text', this.dropdown).text('Choose a template'); $('.dropdown-toggle-text', this.dropdown).text('Choose a template');
}); });
......
...@@ -163,6 +163,10 @@ ul.content-list { ...@@ -163,6 +163,10 @@ ul.content-list {
&:last-child { &:last-child {
margin-right: 0; margin-right: 0;
@media(max-width: $screen-xs-max) {
margin: 0 auto;
}
} }
} }
......
...@@ -74,6 +74,7 @@ ...@@ -74,6 +74,7 @@
height: 475px; // Needed for PhantomJS height: 475px; // Needed for PhantomJS
height: calc(100vh - 220px); height: calc(100vh - 220px);
min-height: 475px; min-height: 475px;
transition: width .2s;
&.is-compact { &.is-compact {
width: calc(100% - 290px); width: calc(100% - 290px);
...@@ -338,3 +339,18 @@ ...@@ -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 @@ ...@@ -25,7 +25,7 @@
} }
.form-horizontal { .form-horizontal {
margin-top: 5px; margin-top: 20px;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
display: -webkit-flex; display: -webkit-flex;
...@@ -98,6 +98,10 @@ ...@@ -98,6 +98,10 @@
padding-right: 35px; padding-right: 35px;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
width: 250px;
}
@media (min-width: $screen-md-min) {
width: 350px; 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 class Dashboard::MilestonesController < Dashboard::ApplicationController
include GlobalMilestones
before_action :projects before_action :projects
before_action :milestone, only: [:show] before_action :milestone, only: [:show]
...@@ -17,4 +15,15 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController ...@@ -17,4 +15,15 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
def show def show
end 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 end
class Groups::MilestonesController < Groups::ApplicationController class Groups::MilestonesController < Groups::ApplicationController
include GlobalMilestones
before_action :group_projects before_action :group_projects
before_action :milestone, only: [:show, :update] before_action :milestone, only: [:show, :update]
before_action :authorize_admin_milestones!, only: [:new, :create, :update] before_action :authorize_admin_milestones!, only: [:new, :create, :update]
...@@ -73,4 +71,13 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -73,4 +71,13 @@ class Groups::MilestonesController < Groups::ApplicationController
def milestone_path(title) def milestone_path(title)
group_milestone_path(@group, title.to_slug.to_s, title: title) group_milestone_path(@group, title.to_slug.to_s, title: title)
end 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 end
...@@ -4,10 +4,7 @@ class Projects::GroupLinksController < Projects::ApplicationController ...@@ -4,10 +4,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
before_action :authorize_admin_project_member!, only: [:update] before_action :authorize_admin_project_member!, only: [:update]
def index def index
@group_links = project.project_group_links.all redirect_to namespace_project_settings_members_path
@skip_groups = @group_links.pluck(:group_id)
@skip_groups << project.namespace_id unless project.personal?
end end
def create def create
...@@ -25,7 +22,7 @@ class Projects::GroupLinksController < Projects::ApplicationController ...@@ -25,7 +22,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
flash[:alert] = 'Please select a group.' flash[:alert] = 'Please select a group.'
end end
redirect_to namespace_project_group_links_path(project.namespace, project) redirect_to namespace_project_settings_members_path(project.namespace, project)
end end
def update def update
...@@ -39,7 +36,7 @@ class Projects::GroupLinksController < Projects::ApplicationController ...@@ -39,7 +36,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
redirect_to namespace_project_group_links_path(project.namespace, project) redirect_to namespace_project_settings_members_path(project.namespace, project)
end end
format.js { head :ok } format.js { head :ok }
end end
......
...@@ -6,54 +6,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -6,54 +6,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController
before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access] before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
def index def index
@sort = params[:sort].presence || sort_value_name sort = params[:sort].presence || sort_value_name
@group_links = @project.project_group_links redirect_to namespace_project_settings_members_path(@project.namespace, @project, sort: sort)
@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
end end
def create def create
status = Members::CreateService.new(@project, current_user, params).execute 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 if status
redirect_to redirect_url, notice: 'Users were successfully added.' redirect_to redirect_url, notice: 'Users were successfully added.'
...@@ -76,14 +36,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -76,14 +36,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
redirect_to namespace_project_project_members_path(@project.namespace, @project) redirect_to namespace_project_settings_members_path(@project.namespace, @project)
end end
format.js { head :ok } format.js { head :ok }
end end
end end
def resend_invite 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]) @project_member = @project.project_members.find(params[:id])
...@@ -106,7 +66,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -106,7 +66,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
return render_404 return render_404
end end
redirect_to(namespace_project_project_members_path(project.namespace, project), redirect_to(namespace_project_settings_members_path(project.namespace, project),
notice: notice) notice: notice)
end 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 ...@@ -206,4 +206,9 @@ module GitlabRoutingHelper
file_namespace_project_build_artifacts_path(*args) file_namespace_project_build_artifacts_path(*args)
end end
end end
# Settings
def project_settings_members_path(project, *args)
namespace_project_settings_members_path(project.namespace, project, *args)
end
end end
...@@ -39,7 +39,7 @@ module SearchHelper ...@@ -39,7 +39,7 @@ module SearchHelper
# Autocomplete results for various settings pages # Autocomplete results for various settings pages
def default_autocomplete 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: "SSH Keys", url: profile_keys_path },
{ category: "Settings", label: "Dashboard", url: root_path }, { category: "Settings", label: "Dashboard", url: root_path },
{ category: "Settings", label: "Admin Section", url: admin_root_path }, { category: "Settings", label: "Admin Section", url: admin_root_path },
...@@ -75,7 +75,7 @@ module SearchHelper ...@@ -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: "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: "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: "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) }, { category: "Current Project", label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) },
] ]
else else
......
...@@ -36,8 +36,8 @@ module Milestoneish ...@@ -36,8 +36,8 @@ module Milestoneish
def issues_visible_to_user(user) def issues_visible_to_user(user)
memoize_per_user(user, :issues_visible_to_user) do memoize_per_user(user, :issues_visible_to_user) do
params = try(:project_id) ? { project_id: project_id } : {} IssuesFinder.new(user, issues_finder_params)
IssuesFinder.new(user, params).execute.where(milestone_id: milestoneish_ids) .execute.where(milestone_id: milestoneish_ids)
end end
end end
...@@ -72,4 +72,10 @@ module Milestoneish ...@@ -72,4 +72,10 @@ module Milestoneish
@memoized[method_name] ||= {} @memoized[method_name] ||= {}
@memoized[method_name][user.try!(:id)] ||= yield @memoized[method_name][user.try!(:id)] ||= yield
end end
# override in a class that includes this module to get a faster query
# from IssuesFinder
def issues_finder_params
{}
end
end end
class GlobalMilestone class GlobalMilestone
include Milestoneish include Milestoneish
EPOCH = DateTime.parse('1970-01-01')
attr_accessor :title, :milestones attr_accessor :title, :milestones
alias_attribute :name, :title alias_attribute :name, :title
...@@ -8,13 +10,22 @@ class GlobalMilestone ...@@ -8,13 +10,22 @@ class GlobalMilestone
@first_milestone @first_milestone
end end
def self.build_collection(milestones) def self.build_collection(projects, params)
milestones = milestones.group_by(&:title) child_milestones = MilestonesFinder.new.execute(projects, params)
milestones.map do |title, milestones| milestones = child_milestones.select(:id, :title).group_by(&:title).map do |title, grouped|
milestones_relation = Milestone.where(id: milestones.map(&:id)) milestones_relation = Milestone.where(id: grouped.map(&:id))
new(title, milestones_relation) new(title, milestones_relation)
end 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 end
def initialize(title, milestones) 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 ...@@ -49,6 +49,10 @@ class Key < ActiveRecord::Base
"key-#{id}" "key-#{id}"
end end
def update_last_used_at
UseKeyWorker.perform_async(self.id)
end
def add_to_shell def add_to_shell
GitlabShellWorker.perform_async( GitlabShellWorker.perform_async(
:add_key, :add_key,
......
...@@ -221,7 +221,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -221,7 +221,7 @@ class MergeRequest < ActiveRecord::Base
# true base commit, so we can't simply have `#diff_base_commit` fall back on # true base commit, so we can't simply have `#diff_base_commit` fall back on
# this method. # this method.
def likely_diff_base_commit def likely_diff_base_commit
first_commit.parent || first_commit first_commit.try(:parent) || first_commit
end end
def diff_start_commit def diff_start_commit
......
...@@ -202,4 +202,8 @@ class Milestone < ActiveRecord::Base ...@@ -202,4 +202,8 @@ class Milestone < ActiveRecord::Base
errors.add(:start_date, "Can't be greater than due date") errors.add(:start_date, "Can't be greater than due date")
end end
end end
def issues_finder_params
{ project_id: project_id }
end
end end
...@@ -37,6 +37,10 @@ class NotificationSetting < ActiveRecord::Base ...@@ -37,6 +37,10 @@ class NotificationSetting < ActiveRecord::Base
:success_pipeline :success_pipeline
] ]
EXCLUDED_WATCHER_EVENTS = [
:success_pipeline
]
store :events, accessors: EMAIL_EVENTS, coder: JSON store :events, accessors: EMAIL_EVENTS, coder: JSON
before_create :set_events before_create :set_events
......
...@@ -591,7 +591,10 @@ class NotificationService ...@@ -591,7 +591,10 @@ class NotificationService
custom_action = build_custom_key(action, target) custom_action = build_custom_key(action, target)
recipients = target.participants(current_user) recipients = target.participants(current_user)
unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
recipients = add_project_watchers(recipients, project) recipients = add_project_watchers(recipients, project)
end
recipients = add_custom_notifications(recipients, project, custom_action) recipients = add_custom_notifications(recipients, project, custom_action)
recipients = reject_mention_users(recipients, project) recipients = reject_mention_users(recipients, project)
......
...@@ -36,7 +36,7 @@ module Projects ...@@ -36,7 +36,7 @@ module Projects
def groups def groups
current_user.authorized_groups.sort_by(&:path).map do |group| current_user.authorized_groups.sort_by(&:path).map do |group|
count = group.users.count 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
end end
......
...@@ -15,7 +15,7 @@ class ProjectPathValidator < ActiveModel::EachValidator ...@@ -15,7 +15,7 @@ class ProjectPathValidator < ActiveModel::EachValidator
# 'tree' as project name and 'deploy_keys' as route. # 'tree' as project name and 'deploy_keys' as route.
# #
RESERVED = (NamespaceValidator::RESERVED - 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 %w[tree commits wikis new edit create update logs_tree
preview blob blame raw files create_dir find_file]).freeze preview blob blame raw files create_dir find_file]).freeze
......
...@@ -44,7 +44,7 @@ ...@@ -44,7 +44,7 @@
%li %li
= link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username } = link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username }
%li %li
= link_to "Profile Settings", profile_path, aria: { label: "Profile Settings" } = link_to "Settings", profile_path, aria: { label: "Settings" }
%li %li
= link_to "Help", help_path, aria: { label: "Help" } = link_to "Help", help_path, aria: { label: "Help" }
%li.divider %li.divider
......
- if project_nav_tab? :team - if project_nav_tab? :team
= nav_link(controller: [:project_members, :teams]) do = nav_link(controller: [:members, :teams]) do
= link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do = link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
%span %span
Members Members
- if can_edit - 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 = nav_link(controller: :deploy_keys) do
= link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
%span %span
......
- page_title "Profile Settings" - page_title "User Settings"
- header_title "Profile Settings", profile_path unless header_title - header_title "User Settings", profile_path unless header_title
- sidebar "dashboard" - sidebar "dashboard"
- nav "profile" - nav "profile"
......
...@@ -6,6 +6,9 @@ ...@@ -6,6 +6,9 @@
= key.title = key.title
.description .description
= key.fingerprint = key.fingerprint
.last-used-at
last used:
= key.last_used_at ? time_ago_with_tooltip(key.last_used_at) : 'n/a'
.pull-right .pull-right
%span.key-created-at %span.key-created-at
created #{time_ago_with_tooltip(key.created_at)} created #{time_ago_with_tooltip(key.created_at)}
......
...@@ -11,6 +11,9 @@ ...@@ -11,6 +11,9 @@
%li %li
%span.light Created on: %span.light Created on:
%strong= @key.created_at.to_s(:medium) %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 .col-md-8
%p %p
......
%board-sidebar{ "inline-template" => true, %board-sidebar{ "inline-template" => true,
":current-user" => "#{current_user ? current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) : {}}" } ":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" } %aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" }
.issuable-sidebar .issuable-sidebar
.block.issuable-sidebar-header .block.issuable-sidebar-header
......
...@@ -20,10 +20,10 @@ ...@@ -20,10 +20,10 @@
.form-group .form-group
= label_tag :expires_at, 'Access expiration date', class: 'label-light' = label_tag :expires_at, 'Access expiration date', class: 'label-light'
.clearable-input .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 %i.clear-icon.js-clear-input
.help-block .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" = submit_tag "Share", class: "btn btn-create"
.col-lg-9.col-lg-offset-3 .col-lg-9.col-lg-offset-3
%hr %hr
......
...@@ -50,7 +50,6 @@ ...@@ -50,7 +50,6 @@
.content-block.content-block-small .content-block.content-block-small
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true = 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') } .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
%div{ class: container_class } %div{ class: container_class }
%ul.merge-request-tabs.nav-links.no-top.no-bottom %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 = render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch
.note-edit-form .note-edit-form
= form_tag '#', method: :put, remote: true, class: 'edit-note common-note-form js-quick-submit' do = form_tag '#', method: :put, class: 'edit-note common-note-form js-quick-submit' do
= hidden_field_tag :authenticity_token, form_authenticity_token
= hidden_field_tag :target_id, '', class: 'js-form-target-id' = hidden_field_tag :target_id, '', class: 'js-form-target-id'
= hidden_field_tag :target_type, '', class: 'js-form-target-type' = hidden_field_tag :target_type, '', class: 'js-form-target-type'
= render layout: 'projects/md_preview', locals: { preview_class: 'md-preview', referenced_users: true } do = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview', referenced_users: true } do
......
...@@ -23,4 +23,4 @@ ...@@ -23,4 +23,4 @@
to post a comment to post a comment
:javascript :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| = form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'users-project-form' } do |f|
.row .form-group
.col-md-4.col-lg-6 = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true, placeholder: "Search for members to update or invite")
= users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true)
.help-block.append-bottom-10 .help-block.append-bottom-10
Search for users by name, username, or email, or invite new ones using their email address. Search for members by name, username, or email, or invite new ones using their email address.
.form-group
.col-md-3.col-lg-2
= select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select" = 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 .help-block.append-bottom-10
= link_to "Read more", help_page_path("user/permissions"), class: "vlink" = link_to "Read more", help_page_path("user/permissions"), class: "vlink"
about role permissions about role permissions
.form-group
.col-md-3.col-lg-2
.clearable-input .clearable-input
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
%i.clear-icon.js-clear-input %i.clear-icon.js-clear-input
.help-block.append-bottom-10 .help-block.append-bottom-10
On this date, the user(s) will automatically lose access to this project. On this date, the member(s) will automatically lose access to this project.
= f.submit "Add to project", class: "btn btn-create"
.col-md-2 = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default", title: "Import members from another project"
= f.submit "Add to project", class: "btn btn-create btn-block"
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
Users with access to Members with access to
%strong #{@project.name} %strong #{@project.name}
%span.badge= @project_members.total_count %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 %ul.content-list
= render partial: 'shared/members/member', collection: members, as: :member = render partial: 'shared/members/member', collection: members, as: :member
...@@ -12,5 +12,4 @@ ...@@ -12,5 +12,4 @@
.form-actions .form-actions
= button_tag 'Import project members', class: "btn btn-create" = 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 @@ ...@@ -97,7 +97,7 @@
remove due date remove due date
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
.selectbox.hide-collapsed .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 .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) } } %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 %span.dropdown-toggle-text Due date
......
...@@ -37,7 +37,6 @@ ...@@ -37,7 +37,6 @@
%i.clear-icon.js-clear-input %i.clear-icon.js-clear-input
- if can_admin_member - if can_admin_member
= link_to namespace_project_group_link_path(@project.namespace, @project, group_link), = link_to namespace_project_group_link_path(@project.namespace, @project, group_link),
remote: true,
method: :delete, method: :delete,
data: { confirm: "Are you sure you want to remove #{group.name}?" }, data: { confirm: "Are you sure you want to remove #{group.name}?" },
class: 'btn btn-remove prepend-left-10' do 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: merge_request:
author: 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 ...@@ -307,6 +307,10 @@ constraints(ProjectUrlConstrainer.new) do
end end
end end
namespace :settings do
resource :members, only: [:show]
end
# Since both wiki and repository routing contains wildcard characters # Since both wiki and repository routing contains wildcard characters
# its preferable to keep it below all other project routes # its preferable to keep it below all other project routes
draw :wiki draw :wiki
......
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
- [email_receiver, 2] - [email_receiver, 2]
- [emails_on_push, 2] - [emails_on_push, 2]
- [mailers, 2] - [mailers, 2]
- [use_key, 1]
- [repository_fork, 1] - [repository_fork, 1]
- [repository_import, 1] - [repository_import, 1]
- [project_service, 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 ...@@ -10,7 +10,6 @@ class RenameReservedProjectNames < ActiveRecord::Migration
KNOWN_PATHS = %w(.well-known KNOWN_PATHS = %w(.well-known
all all
assets
blame blame
blob blob
commits commits
...@@ -18,7 +17,6 @@ class RenameReservedProjectNames < ActiveRecord::Migration ...@@ -18,7 +17,6 @@ class RenameReservedProjectNames < ActiveRecord::Migration
create_dir create_dir
edit edit
files files
files
find_file find_file
groups groups
hooks hooks
...@@ -26,11 +24,8 @@ class RenameReservedProjectNames < ActiveRecord::Migration ...@@ -26,11 +24,8 @@ class RenameReservedProjectNames < ActiveRecord::Migration
logs_tree logs_tree
merge_requests merge_requests
new new
new
preview preview
profile
projects projects
public
raw raw
repository repository
robots.txt robots.txt
......
...@@ -528,6 +528,7 @@ ActiveRecord::Schema.define(version: 20161227192806) do ...@@ -528,6 +528,7 @@ ActiveRecord::Schema.define(version: 20161227192806) do
t.string "fingerprint" t.string "fingerprint"
t.boolean "public", default: false, null: false t.boolean "public", default: false, null: false
t.boolean "can_push", default: false, null: false t.boolean "can_push", default: false, null: false
t.datetime "last_used_at"
end end
add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree 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 ...@@ -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 GitLab. Common combinations are `method: 'plain'` and `port: 389`, OR
`method: 'ssl'` and `port: 636`. `method: 'ssl'` and `port: 636`.
### Login with valid credentials rejected ### Troubleshooting
If there is an unexpected error while authenticating the user with the LDAP If a user account is blocked or unblocked due to the LDAP configuration, a
backend, the login is rejected and details about the error are logged to 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`. `production.log`.
...@@ -71,8 +71,8 @@ If you want to use Gmail / Google Apps with Reply by email, make sure you have ...@@ -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) [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). 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 To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
[these instructions](./postfix.md). [Postfix setup documentation](reply_by_email_postfix_setup.md).
### Omnibus package installations ### Omnibus package installations
......
...@@ -4,12 +4,14 @@ The copy for GitLab is clear and direct. We strike a clear balance between profe ...@@ -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. 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:** >**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. 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 ## Contents
* [Brevity](#brevity) * [Brevity](#brevity)
* [Sentence case](#sentence-case) * [Capitalization and punctuation](#capitalization-and-punctuation)
* [Terminology](#terminology) * [Terminology](#terminology)
--- ---
...@@ -29,8 +31,71 @@ Preferrably use context and placement of controls to make it obvious what clicki ...@@ -29,8 +31,71 @@ Preferrably use context and placement of controls to make it obvious what clicki
--- ---
## Sentence case ## Capitalization and punctuation
Use sentence case for all titles, headings, labels, menu items, and buttons.
### 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. ...@@ -48,15 +113,15 @@ Only use the terms in the tables below.
| Deleted | | Deleted |
>**Example:** >**Example:**
Use `5 open issues` and don't use `5 pending issues`. Use `5 open issues` and dont use `5 pending issues`.
#### Verbs (actions) #### 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 || | 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 || | Close | Close an open issue ||
| Re-open | Re-open a closed issue | There should never be a need to use `open` as a verb | | 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 || | 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 ...@@ -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) ![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) ![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 ...@@ -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) ![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) ![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 ...@@ -93,7 +158,7 @@ The form should be titled `Edit issue`. The submit button should be labeled `Sav
#### Verbs (actions) #### Verbs (actions)
| Term | Use | Don't | | Term | Use | Dont |
| ---- | --- | --- | | ---- | --- | --- |
| Add | Add a merge request | Do not use `create` or `new` | | Add | Add a merge request | Do not use `create` or `new` |
| View | View an open or merged merge request || | 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 ...@@ -105,7 +170,18 @@ The form should be titled `Edit issue`. The submit button should be labeled `Sav
### Comments & Discussions ### Comments & Discussions
#### Comment #### 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 [material design]: https://material.io/guidelines/
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. [features]: https://about.gitlab.com/features/ "GitLab features page"
\ No newline at end of file [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 ...@@ -271,9 +271,9 @@ sudo usermod -aG redis git
### Clone the Source ### Clone the Source
# Clone GitLab repository # 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 ### Configure It
...@@ -400,16 +400,10 @@ GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/). The ...@@ -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` following command-line will install GitLab-Workhorse in `/home/git/gitlab-workhorse`
which is the recommended location. 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 sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
### Initialize Database and Activate Advanced Features ### 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 sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production
# Type 'yes' to create the database tables. # Type 'yes' to create the database tables.
......
...@@ -88,7 +88,7 @@ It uses the [Fog library](http://fog.io/) to perform the upload. ...@@ -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 In the example below we use Amazon S3 for storage, but Fog also lets you use
[other storage providers](http://fog.io/storage/). GitLab [other storage providers](http://fog.io/storage/). GitLab
[imports cloud drivers](https://gitlab.com/gitlab-org/gitlab-ce/blob/30f5b9a5b711b46f1065baf755e413ceced5646b/Gemfile#L88) [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). [also available](#uploading-to-locally-mounted-shares).
For omnibus packages: For omnibus packages:
......
...@@ -11,12 +11,15 @@ guide links by version. ...@@ -11,12 +11,15 @@ guide links by version.
### 1. Stop server ### 1. Stop server
sudo service gitlab stop ```bash
sudo service gitlab stop
```
### 2. Backup ### 2. Backup
```bash ```bash
cd /home/git/gitlab cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production 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 ...@@ -49,6 +52,8 @@ sudo gem install bundler --no-ri --no-rdoc
### 4. Get latest code ### 4. Get latest code
```bash ```bash
cd /home/git/gitlab
sudo -u git -H git fetch --all sudo -u git -H git fetch --all
sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically 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 ...@@ -56,6 +61,8 @@ sudo -u git -H git checkout -- db/schema.rb # local changes will be restored aut
For GitLab Community Edition: For GitLab Community Edition:
```bash ```bash
cd /home/git/gitlab
sudo -u git -H git checkout 8-15-stable sudo -u git -H git checkout 8-15-stable
``` ```
...@@ -64,28 +71,12 @@ OR ...@@ -64,28 +71,12 @@ OR
For GitLab Enterprise Edition: For GitLab Enterprise Edition:
```bash ```bash
sudo -u git -H git checkout 8-15-stable-ee cd /home/git/gitlab
```
### 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.
```bash sudo -u git -H git checkout 8-15-stable-ee
sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
``` ```
### 7. Install libs, migrations, etc. ### 5. Install libs, migrations, etc.
```bash ```bash
cd /home/git/gitlab cd /home/git/gitlab
...@@ -106,6 +97,27 @@ sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production ...@@ -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 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 ### 8. Update configuration files
#### New configuration options for `gitlab.yml` #### New configuration options for `gitlab.yml`
...@@ -113,6 +125,8 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS ...@@ -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`: 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 ```sh
cd /home/git/gitlab
git diff origin/8-14-stable:config/gitlab.yml.example origin/8-15-stable:config/gitlab.yml.example 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 ...@@ -122,6 +136,8 @@ Configure Git to generate packfile bitmaps (introduced in Git 2.0) on
the GitLab server during `git gc`. the GitLab server during `git gc`.
```sh ```sh
cd /home/git/gitlab
sudo -u git -H git config --global repack.writeBitmaps true sudo -u git -H git config --global repack.writeBitmaps true
``` ```
...@@ -130,6 +146,8 @@ 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: Ensure you're still up-to-date with the latest NGINX configuration changes:
```sh ```sh
cd /home/git/gitlab
# For HTTPS configurations # For HTTPS configurations
git diff origin/8-14-stable:lib/support/nginx/gitlab-ssl origin/8-15-stable:lib/support/nginx/gitlab-ssl 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. ...@@ -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: 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: For Ubuntu 16.04.1 LTS:
sudo systemctl daemon-reload ```bash
sudo systemctl daemon-reload
```
### 9. Start application ### 9. Start application
sudo service gitlab start ```bash
sudo service nginx restart sudo service gitlab start
sudo service nginx restart
```
### 10. Check application status ### 10. Check application status
Check if GitLab and its environment are configured correctly: 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: 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! 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). ...@@ -196,6 +230,7 @@ database migration (the backup is already migrated to the previous version).
```bash ```bash
cd /home/git/gitlab cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production 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) ...@@ -14,6 +14,7 @@ user on the database version)
```bash ```bash
cd /home/git/gitlab cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
``` ```
...@@ -32,28 +33,13 @@ current version with `cat VERSION`). ...@@ -32,28 +33,13 @@ current version with `cat VERSION`).
```bash ```bash
cd /home/git/gitlab cd /home/git/gitlab
sudo -u git -H git fetch --all sudo -u git -H git fetch --all
sudo -u git -H git checkout -- Gemfile.lock db/schema.rb sudo -u git -H git checkout -- Gemfile.lock db/schema.rb
sudo -u git -H git checkout LATEST_TAG -b LATEST_TAG sudo -u git -H git checkout LATEST_TAG -b LATEST_TAG
``` ```
### 3. Update gitlab-shell to the corresponding version ### 3. Install libs, migrations, etc.
```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.
```bash ```bash
cd /home/git/gitlab cd /home/git/gitlab
...@@ -74,6 +60,23 @@ sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production ...@@ -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 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 ### 6. Start application
```bash ```bash
...@@ -86,6 +89,8 @@ sudo service nginx restart ...@@ -86,6 +89,8 @@ sudo service nginx restart
Check if GitLab and its environment are configured correctly: Check if GitLab and its environment are configured correctly:
```bash ```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production 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. ...@@ -5,8 +5,7 @@ Import your projects from Gitea to GitLab with minimal effort.
## Overview ## Overview
>**Note:** >**Note:**
As of Gitea `v1.0.0`, issue & pull-request comments cannot be imported! This is This requires Gitea `v1.0.0` or newer.
a [known issue][issue-401] that should be fixed in a near-future.
- At its current state, Gitea importer can import: - At its current state, Gitea importer can import:
- the repository description (GitLab 8.15+) - 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 ...@@ -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, You can also choose a different name for the project and a different namespace,
if you have the privileges to do so. 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: ...@@ -73,7 +73,7 @@ In all of the below cases, the notification will be sent to:
...with notification level "Participating" or higher ...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 - 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 - 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 ...@@ -114,8 +114,10 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end end
step 'I click link "Import team from another project"' do step 'I click link "Import team from another project"' do
page.within '.users-project-form' do
click_link "Import" click_link "Import"
end end
end
When 'I submit "Website" project for import team' do When 'I submit "Website" project for import team' do
project = Project.find_by(name: "Website") project = Project.find_by(name: "Website")
......
...@@ -14,7 +14,11 @@ module API ...@@ -14,7 +14,11 @@ module API
end end
# Retain 405 error rather than a 500 error for Grape 0.15.0+. # 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| rescue_from Grape::Exceptions::Base do |e|
error! e.message, e.status, e.headers error! e.message, e.status, e.headers
end end
......
...@@ -28,6 +28,8 @@ module API ...@@ -28,6 +28,8 @@ module API
protocol = params[:protocol] protocol = params[:protocol]
actor.update_last_used_at if actor.is_a?(Key)
access = access =
if wiki? if wiki?
Gitlab::GitAccessWiki.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities) Gitlab::GitAccessWiki.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
...@@ -61,6 +63,8 @@ module API ...@@ -61,6 +63,8 @@ module API
status 200 status 200
key = Key.find(params[:key_id]) key = Key.find(params[:key_id])
key.update_last_used_at
token_handler = Gitlab::LfsToken.new(key) token_handler = Gitlab::LfsToken.new(key)
{ {
...@@ -103,7 +107,9 @@ module API ...@@ -103,7 +107,9 @@ module API
key = Key.find_by(id: params[:key_id]) 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' } return { 'success' => false, 'message' => 'Could not find the given key' }
end end
......
...@@ -91,10 +91,11 @@ module API ...@@ -91,10 +91,11 @@ module API
authenticated_as_admin! authenticated_as_admin!
# Filter out params which are used later # 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) 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 user.skip_confirmation! unless confirm
if identity_attrs.any? if identity_attrs.any?
...@@ -159,11 +160,7 @@ module API ...@@ -159,11 +160,7 @@ module API
end end
end end
# Delete already handled parameters if user.update_attributes(user_params.except(:extern_uid, :provider))
user_params.delete(:extern_uid)
user_params.delete(:provider)
if user.update_attributes(user_params)
present user, with: Entities::UserPublic present user, with: Entities::UserPublic
else else
render_validation_error!(user) render_validation_error!(user)
......
...@@ -105,7 +105,7 @@ module Ci ...@@ -105,7 +105,7 @@ module Ci
break break
elsif s.scan(/</) elsif s.scan(/</)
@out << '&lt;' @out << '&lt;'
elsif s.scan(/\n/) elsif s.scan(/\r?\n/)
@out << '<br>' @out << '<br>'
else else
@out << s.scan(/./m) @out << s.scan(/./m)
......
...@@ -8,6 +8,16 @@ module Ci ...@@ -8,6 +8,16 @@ module Ci
rack_response({ 'message' => '404 Not found' }.to_json, 404) rack_response({ 'message' => '404 Not found' }.to_json, 404)
end 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| rescue_from :all do |exception|
handle_api_exception(exception) handle_api_exception(exception)
end end
......
...@@ -34,21 +34,21 @@ module Gitlab ...@@ -34,21 +34,21 @@ module Gitlab
def allowed? def allowed?
if ldap_user if ldap_user
unless ldap_config.active_directory unless ldap_config.active_directory
user.activate if user.ldap_blocked? unblock_user(user, 'is available again') if user.ldap_blocked?
return true return true
end end
# Block user in GitLab if he/she was blocked in AD # 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) 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 false
else else
user.activate if user.ldap_blocked? unblock_user(user, 'is not disabled anymore') if user.ldap_blocked?
true true
end end
else else
# Block the user if they no longer exist in LDAP/AD # Block the user if they no longer exist in LDAP/AD
user.ldap_block block_user(user, 'does not exist anymore')
false false
end end
end end
...@@ -64,6 +64,24 @@ module Gitlab ...@@ -64,6 +64,24 @@ module Gitlab
def ldap_user def ldap_user
@ldap_user ||= Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter) @ldap_user ||= Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter)
end 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 end
end end
...@@ -25,7 +25,7 @@ module Gitlab ...@@ -25,7 +25,7 @@ module Gitlab
end end
def get_raw(key) def get_raw(key)
auth_hash.extra[:raw_info][key] auth_hash.extra[:raw_info][key] if auth_hash.extra
end end
def ldap_config def ldap_config
......
...@@ -107,7 +107,7 @@ module Gitlab ...@@ -107,7 +107,7 @@ module Gitlab
end end
def attributes def attributes
options['attributes'] default_attributes.merge(options['attributes'])
end end
def timeout def timeout
...@@ -130,6 +130,16 @@ module Gitlab ...@@ -130,6 +130,16 @@ module Gitlab
end end
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 protected
def base_options def base_options
......
...@@ -28,7 +28,7 @@ module Gitlab ...@@ -28,7 +28,7 @@ module Gitlab
end end
def name def name
attribute_value(:name) attribute_value(:name).first
end end
def uid def uid
...@@ -62,14 +62,12 @@ module Gitlab ...@@ -62,14 +62,12 @@ module Gitlab
# this method looks for 'mail', 'email' and 'userPrincipalName' and # this method looks for 'mail', 'email' and 'userPrincipalName' and
# returns the first with a value. # returns the first with a value.
def attribute_value(attribute) 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) } selected_attr = attributes.find { |attr| entry.respond_to?(attr) }
return nil unless selected_attr return nil unless selected_attr
# Some LDAP attributes return an array, entry.public_send(selected_attr)
# even if it is a single value (like 'cn')
Array(entry.public_send(selected_attr)).first
end end
end end
end end
......
...@@ -70,9 +70,13 @@ module Gitlab ...@@ -70,9 +70,13 @@ module Gitlab
def tag_endpoint(trans, env) def tag_endpoint(trans, env)
endpoint = env[ENDPOINT_KEY] 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] path = endpoint_paths_cache[endpoint.route.request_method][endpoint.route.path]
trans.action = "Grape##{endpoint.route.request_method} #{path}" trans.action = "Grape##{endpoint.route.request_method} #{path}"
end end
end
private private
......
...@@ -31,7 +31,7 @@ describe Projects::GroupLinksController do ...@@ -31,7 +31,7 @@ describe Projects::GroupLinksController do
it 'redirects to project group links page' do it 'redirects to project group links page' do
expect(response).to redirect_to( expect(response).to redirect_to(
namespace_project_group_links_path(project.namespace, project) namespace_project_settings_members_path(project.namespace, project)
) )
end end
end end
...@@ -62,7 +62,7 @@ describe Projects::GroupLinksController do ...@@ -62,7 +62,7 @@ describe Projects::GroupLinksController do
it 'redirects to project group links page' do it 'redirects to project group links page' do
expect(response).to redirect_to( expect(response).to redirect_to(
namespace_project_group_links_path(project.namespace, project) namespace_project_settings_members_path(project.namespace, project)
) )
end end
end end
...@@ -76,7 +76,7 @@ describe Projects::GroupLinksController do ...@@ -76,7 +76,7 @@ describe Projects::GroupLinksController do
it 'redirects to project group links page' do it 'redirects to project group links page' do
expect(response).to redirect_to( 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.') expect(flash[:alert]).to eq('Please select a group.')
end end
......
...@@ -5,11 +5,11 @@ describe Projects::ProjectMembersController do ...@@ -5,11 +5,11 @@ describe Projects::ProjectMembersController do
let(:project) { create(:empty_project, :public, :access_requestable) } let(:project) { create(:empty_project, :public, :access_requestable) }
describe 'GET index' do 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 get :index, namespace_id: project.namespace, project_id: project
expect(response).to have_http_status(200) expect(response).to have_http_status(302)
expect(response).to render_template(:index) expect(response.location).to include namespace_project_settings_members_path(project.namespace, project)
end end
end end
...@@ -44,7 +44,7 @@ describe Projects::ProjectMembersController do ...@@ -44,7 +44,7 @@ describe Projects::ProjectMembersController do
access_level: Gitlab::Access::GUEST access_level: Gitlab::Access::GUEST
expect(response).to set_flash.to 'Users were successfully added.' 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 end
it 'adds no user to members' do it 'adds no user to members' do
...@@ -56,7 +56,7 @@ describe Projects::ProjectMembersController do ...@@ -56,7 +56,7 @@ describe Projects::ProjectMembersController do
access_level: Gitlab::Access::GUEST access_level: Gitlab::Access::GUEST
expect(response).to set_flash.to 'No users or groups specified.' 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 end
end end
...@@ -99,7 +99,7 @@ describe Projects::ProjectMembersController do ...@@ -99,7 +99,7 @@ describe Projects::ProjectMembersController do
id: member id: member
expect(response).to redirect_to( 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 expect(project.members).not_to include member
end end
...@@ -259,7 +259,7 @@ describe Projects::ProjectMembersController do ...@@ -259,7 +259,7 @@ describe Projects::ProjectMembersController do
expect(project.team_members).to include member expect(project.team_members).to include member
expect(response).to set_flash.to 'Successfully imported' expect(response).to set_flash.to 'Successfully imported'
expect(response).to redirect_to( expect(response).to redirect_to(
namespace_project_project_members_path(project.namespace, project) namespace_project_settings_members_path(project.namespace, project)
) )
end end
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 ...@@ -619,14 +619,18 @@ describe 'Issues', feature: true do
end end
it 'adds due date to issue' do it 'adds due date to issue' do
date = Date.today.at_beginning_of_month + 2.days
page.within '.due_date' do page.within '.due_date' do
click_link 'Edit' click_link 'Edit'
page.within '.ui-datepicker-calendar' do page.within '.ui-datepicker-calendar' do
first('.ui-state-default').click click_link date.day
end 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
end end
...@@ -638,6 +642,8 @@ describe 'Issues', feature: true do ...@@ -638,6 +642,8 @@ describe 'Issues', feature: true do
first('.ui-state-default').click first('.ui-state-default').click
end end
wait_for_ajax
expect(page).to have_no_content 'No due date' expect(page).to have_no_content 'No due date'
click_link 'remove due date' click_link 'remove due date'
......
...@@ -4,6 +4,8 @@ require 'spec_helper' ...@@ -4,6 +4,8 @@ require 'spec_helper'
# message to be shown by JavaScript when the source branch was deleted. # message to be shown by JavaScript when the source branch was deleted.
# Please do not remove "js: true". # Please do not remove "js: true".
describe 'Deleted source branch', feature: true, js: true do describe 'Deleted source branch', feature: true, js: true do
include WaitForAjax
let(:user) { create(:user) } let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) } let(:merge_request) { create(:merge_request) }
...@@ -13,7 +15,8 @@ describe 'Deleted source branch', feature: true, js: true do ...@@ -13,7 +15,8 @@ describe 'Deleted source branch', feature: true, js: true do
merge_request.update!(source_branch: 'this-branch-does-not-exist') merge_request.update!(source_branch: 'this-branch-does-not-exist')
visit namespace_project_merge_request_path( visit namespace_project_merge_request_path(
merge_request.project.namespace, merge_request.project.namespace,
merge_request.project, merge_request merge_request.project,
merge_request
) )
end end
...@@ -23,11 +26,17 @@ describe 'Deleted source branch', feature: true, js: true do ...@@ -23,11 +26,17 @@ describe 'Deleted source branch', feature: true, js: true do
) )
end end
it 'hides Discussion, Commits and Changes tabs' do it 'still contains Discussion, Commits and Changes tabs' do
within '.merge-request-details' do within '.merge-request-details' do
expect(page).to have_no_content('Discussion') expect(page).to have_content('Discussion')
expect(page).to have_no_content('Commits') expect(page).to have_content('Commits')
expect(page).to have_no_content('Changes') expect(page).to have_content('Changes')
end 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
end end
...@@ -14,10 +14,10 @@ feature 'Project group links', feature: true, js: true do ...@@ -14,10 +14,10 @@ feature 'Project group links', feature: true, js: true do
context 'setting an expiration date for a group link' do context 'setting an expiration date for a group link' do
before 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' 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 page.find('body').click
click_on 'Share' click_on 'Share'
end end
......
...@@ -27,7 +27,7 @@ feature 'issuable templates', feature: true, js: true do ...@@ -27,7 +27,7 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects "bug" template' do scenario 'user selects "bug" template' do
select_template 'bug' select_template 'bug'
wait_for_ajax wait_for_ajax
preview_template assert_template
save_changes save_changes
end end
...@@ -35,8 +35,7 @@ feature 'issuable templates', feature: true, js: true do ...@@ -35,8 +35,7 @@ feature 'issuable templates', feature: true, js: true do
select_template 'bug' select_template 'bug'
wait_for_ajax wait_for_ajax
select_option 'No template' select_option 'No template'
wait_for_ajax assert_template('')
preview_template('')
save_changes('') save_changes('')
end end
...@@ -44,9 +43,9 @@ feature 'issuable templates', feature: true, js: true do ...@@ -44,9 +43,9 @@ feature 'issuable templates', feature: true, js: true do
select_template 'bug' select_template 'bug'
wait_for_ajax wait_for_ajax
find_field('issue_description').send_keys(description_addition) find_field('issue_description').send_keys(description_addition)
preview_template(template_content + description_addition) assert_template(template_content + description_addition)
select_option 'Reset template' select_option 'Reset template'
preview_template assert_template
save_changes save_changes
end end
...@@ -77,7 +76,7 @@ feature 'issuable templates', feature: true, js: true do ...@@ -77,7 +76,7 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects "bug" template' do scenario 'user selects "bug" template' do
select_template 'bug' select_template 'bug'
wait_for_ajax wait_for_ajax
preview_template("#{template_content}") assert_template("#{template_content}")
save_changes save_changes
end end
end end
...@@ -95,7 +94,7 @@ feature 'issuable templates', feature: true, js: true do ...@@ -95,7 +94,7 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects "feature-proposal" template' do scenario 'user selects "feature-proposal" template' do
select_template 'feature-proposal' select_template 'feature-proposal'
wait_for_ajax wait_for_ajax
preview_template assert_template
save_changes save_changes
end end
end end
...@@ -122,17 +121,15 @@ feature 'issuable templates', feature: true, js: true do ...@@ -122,17 +121,15 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects template' do scenario 'user selects template' do
select_template 'feature-proposal' select_template 'feature-proposal'
wait_for_ajax wait_for_ajax
preview_template assert_template
save_changes save_changes
end end
end end
end end
end end
def preview_template(expected_content = template_content) def assert_template(expected_content = template_content)
click_link 'Preview' expect(find('textarea')['value']).to eq(expected_content)
expect(page).to have_content expected_content
click_link 'Write'
end end
def save_changes(expected_content = template_content) def save_changes(expected_content = template_content)
......
...@@ -11,10 +11,10 @@ feature 'Projects > Members > Anonymous user sees members', feature: true do ...@@ -11,10 +11,10 @@ feature 'Projects > Members > Anonymous user sees members', feature: true do
end end
scenario "anonymous user visits the project's members page and sees the list of members" do 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( 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) expect(page).to have_content(user.name)
end end
end end
...@@ -12,7 +12,7 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t ...@@ -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) @group_link = create(:project_group_link, project: project, group: group)
login_as(user) login_as(user)
visit namespace_project_project_members_path(project.namespace, project) visit namespace_project_settings_members_path(project.namespace, project)
end end
it 'updates group access level' do it 'updates group access level' do
...@@ -24,7 +24,7 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t ...@@ -24,7 +24,7 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t
wait_for_ajax 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') expect(first('.group_member')).to have_content('Guest')
end end
......
...@@ -19,7 +19,7 @@ feature 'Projects members', feature: true do ...@@ -19,7 +19,7 @@ feature 'Projects members', feature: true do
context 'with a group invitee' do context 'with a group invitee' do
before do before do
group_invitee group_invitee
visit namespace_project_project_members_path(project.namespace, project) visit namespace_project_settings_members_path(project.namespace, project)
end end
scenario 'does not appear in the project members page' do scenario 'does not appear in the project members page' do
...@@ -33,7 +33,7 @@ feature 'Projects members', feature: true do ...@@ -33,7 +33,7 @@ feature 'Projects members', feature: true do
before do before do
group_invitee group_invitee
project_invitee project_invitee
visit namespace_project_project_members_path(project.namespace, project) visit namespace_project_settings_members_path(project.namespace, project)
end end
scenario 'shows the project invitee, the project developer, and the group owner' do scenario 'shows the project invitee, the project developer, and the group owner' do
...@@ -54,7 +54,7 @@ feature 'Projects members', feature: true do ...@@ -54,7 +54,7 @@ feature 'Projects members', feature: true do
context 'with a group requester' do context 'with a group requester' do
before do before do
group.request_access(group_requester) group.request_access(group_requester)
visit namespace_project_project_members_path(project.namespace, project) visit namespace_project_settings_members_path(project.namespace, project)
end end
scenario 'does not appear in the project members page' do scenario 'does not appear in the project members page' do
...@@ -68,7 +68,7 @@ feature 'Projects members', feature: true do ...@@ -68,7 +68,7 @@ feature 'Projects members', feature: true do
before do before do
group.request_access(group_requester) group.request_access(group_requester)
project.request_access(project_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 end
scenario 'shows the project requester, the project developer, and the group owner' do 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: ...@@ -14,15 +14,15 @@ feature 'Projects > Members > Master adds member with expiration date', feature:
login_as(master) login_as(master)
end 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 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 page.within '.users-project-form' do
select2(new_member.id, from: '#user_ids', multiple: true) select2(new_member.id, from: '#user_ids', multiple: true)
fill_in 'expires_at', with: '2016-08-10' fill_in 'expires_at', with: '2016-08-10'
click_on 'Add to project'
end end
find('.users-project-form').click
click_on 'Add to project'
page.within "#project_member_#{new_member.project_members.first.id}" do page.within "#project_member_#{new_member.project_members.first.id}" do
expect(page).to have_content('Expires in 4 days') expect(page).to have_content('Expires in 4 days')
......
...@@ -39,7 +39,7 @@ feature 'Projects > Members > User requests access', feature: true do ...@@ -39,7 +39,7 @@ feature 'Projects > Members > User requests access', feature: true do
open_project_settings_menu open_project_settings_menu
click_link 'Members' 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 page.within('.content') do
expect(page).not_to have_content(user.name) expect(page).not_to have_content(user.name)
end end
......
...@@ -82,8 +82,8 @@ describe "Internal Project Access", feature: true do ...@@ -82,8 +82,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_denied_for(:visitor) } it { is_expected.to be_denied_for(:visitor) }
end end
describe "GET /:project_path/project_members" do describe "GET /:project_path/settings/members" do
subject { namespace_project_project_members_path(project.namespace, project) } subject { namespace_project_settings_members_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:owner).of(project) }
......
...@@ -82,8 +82,8 @@ describe "Private Project Access", feature: true do ...@@ -82,8 +82,8 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_denied_for(:visitor) } it { is_expected.to be_denied_for(:visitor) }
end end
describe "GET /:project_path/project_members" do describe "GET /:project_path/settings/members" do
subject { namespace_project_project_members_path(project.namespace, project) } subject { namespace_project_settings_members_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:owner).of(project) }
......
...@@ -82,8 +82,8 @@ describe "Public Project Access", feature: true do ...@@ -82,8 +82,8 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for(:visitor) } it { is_expected.to be_allowed_for(:visitor) }
end end
describe "GET /:project_path/project_members" do describe "GET /:project_path/settings/members" do
subject { namespace_project_project_members_path(project.namespace, project) } subject { namespace_project_settings_members_path(project.namespace, project) }
it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:owner).of(project) }
......
{ {
"plugins": ["jasmine"],
"env": { "env": {
"jasmine": true "jasmine": true
}, },
"extends": "plugin:jasmine/recommended", "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": { "rules": {
"prefer-arrow-callback": 0, "prefer-arrow-callback": 0,
"func-names": 0 "func-names": 0
},
"globals": {
"fixture": false,
"spyOnEvent": false
} }
} }
...@@ -13,10 +13,10 @@ ...@@ -13,10 +13,10 @@
(index, element) => element.innerText.indexOf(searchText) > -1, (index, element) => element.innerText.indexOf(searchText) > -1,
).first(); ).first();
fixture.preload(FIXTURE); preloadFixtures(FIXTURE);
beforeEach(function () { beforeEach(function () {
fixture.load(FIXTURE); loadFixtures(FIXTURE);
this.abuseReports = new global.AbuseReports(); this.abuseReports = new global.AbuseReports();
messages = $('.abuse-reports .message'); messages = $('.abuse-reports .message');
}); });
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
(() => { (() => {
window.gon || (window.gon = {}); window.gon || (window.gon = {});
const fixtureTemplate = 'event_filter.html'; const fixtureTemplate = 'static/event_filter.html.raw';
const filters = [ const filters = [
{ {
id: 'all', id: 'all',
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
describe('Activities', () => { describe('Activities', () => {
beforeEach(() => { beforeEach(() => {
fixture.load(fixtureTemplate); loadFixtures(fixtureTemplate);
new gl.Activities(); new gl.Activities();
}); });
......
...@@ -34,9 +34,9 @@ ...@@ -34,9 +34,9 @@
}; };
describe('AwardsHandler', function() { describe('AwardsHandler', function() {
fixture.preload('issues/open-issue.html.raw'); preloadFixtures('issues/open-issue.html.raw');
beforeEach(function() { beforeEach(function() {
fixture.load('issues/open-issue.html.raw'); loadFixtures('issues/open-issue.html.raw');
awardsHandler = new AwardsHandler; awardsHandler = new AwardsHandler;
spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) { spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) {
return function(url, emoji, cb) { return function(url, emoji, cb) {
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
describe('Autosize behavior', function() { describe('Autosize behavior', function() {
var load; var load;
beforeEach(function() { 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() { it('does not overwrite the resize property', function() {
load(); load();
......
...@@ -5,9 +5,9 @@ ...@@ -5,9 +5,9 @@
(function() { (function() {
describe('Quick Submit behavior', function() { describe('Quick Submit behavior', function() {
var keydownEvent; var keydownEvent;
fixture.preload('behaviors/quick_submit.html'); preloadFixtures('static/behaviors/quick_submit.html.raw');
beforeEach(function() { beforeEach(function() {
fixture.load('behaviors/quick_submit.html'); loadFixtures('static/behaviors/quick_submit.html.raw');
$('form').submit(function(e) { $('form').submit(function(e) {
// Prevent a form submit from moving us off the testing page // Prevent a form submit from moving us off the testing page
return e.preventDefault(); return e.preventDefault();
......
...@@ -4,9 +4,9 @@ ...@@ -4,9 +4,9 @@
(function() { (function() {
describe('requiresInput', function() { describe('requiresInput', function() {
fixture.preload('behaviors/requires_input.html'); preloadFixtures('static/behaviors/requires_input.html.raw');
beforeEach(function() { 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() { it('disables submit when any field is required', function() {
$('.js-requires-input').requiresInput(); $('.js-requires-input').requiresInput();
......
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
(() => { (() => {
describe('Linked Tabs', () => { describe('Linked Tabs', () => {
fixture.preload('linked_tabs'); preloadFixtures('static/linked_tabs.html.raw');
beforeEach(() => { beforeEach(() => {
fixture.load('linked_tabs'); loadFixtures('static/linked_tabs.html.raw');
}); });
describe('when is initialized', () => { describe('when is initialized', () => {
......
...@@ -17,10 +17,10 @@ describe('Build', () => { ...@@ -17,10 +17,10 @@ describe('Build', () => {
offset: BUILD_TRACE.length, n_open_tags: 0, fg_color: null, bg_color: null, style_mask: 0, 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(() => { beforeEach(() => {
fixture.load('builds/build-with-artifacts.html.raw'); loadFixtures('builds/build-with-artifacts.html.raw');
spyOn($, 'ajax'); spyOn($, 'ajax');
}); });
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
((global) => { ((global) => {
describe('Dashboard', () => { describe('Dashboard', () => {
const fixtureTemplate = 'dashboard.html'; const fixtureTemplate = 'static/dashboard.html.raw';
function todosCountText() { function todosCountText() {
return $('.js-todos-count').text(); return $('.js-todos-count').text();
...@@ -17,9 +17,9 @@ ...@@ -17,9 +17,9 @@
$(document).trigger('todo:toggle', newCount); $(document).trigger('todo:toggle', newCount);
} }
fixture.preload(fixtureTemplate); preloadFixtures(fixtureTemplate);
beforeEach(() => { beforeEach(() => {
fixture.load(fixtureTemplate); loadFixtures(fixtureTemplate);
new global.Sidebar(); new global.Sidebar();
}); });
......
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
//= require environments/components/environment_actions //= require environments/components/environment_actions
describe('Actions Component', () => { describe('Actions Component', () => {
fixture.preload('environments/element.html'); preloadFixtures('static/environments/element.html.raw');
beforeEach(() => { beforeEach(() => {
fixture.load('environments/element.html'); loadFixtures('static/environments/element.html.raw');
}); });
it('should render a dropdown with the provided actions', () => { it('should render a dropdown with the provided actions', () => {
......
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
//= require environments/components/environment_external_url //= require environments/components/environment_external_url
describe('External URL Component', () => { describe('External URL Component', () => {
fixture.preload('environments/element.html'); preloadFixtures('static/environments/element.html.raw');
beforeEach(() => { beforeEach(() => {
fixture.load('environments/element.html'); loadFixtures('static/environments/element.html.raw');
}); });
it('should link to the provided externalUrl prop', () => { it('should link to the provided externalUrl prop', () => {
......
...@@ -3,9 +3,9 @@ ...@@ -3,9 +3,9 @@
//= require environments/components/environment_item //= require environments/components/environment_item
describe('Environment item', () => { describe('Environment item', () => {
fixture.preload('environments/table.html'); preloadFixtures('static/environments/table.html.raw');
beforeEach(() => { beforeEach(() => {
fixture.load('environments/table.html'); loadFixtures('static/environments/table.html.raw');
}); });
describe('When item is folder', () => { describe('When item is folder', () => {
......
//= require vue //= require vue
//= require environments/components/environment_rollback //= require environments/components/environment_rollback
describe('Rollback Component', () => { describe('Rollback Component', () => {
fixture.preload('environments/element.html'); preloadFixtures('static/environments/element.html.raw');
const retryURL = 'https://gitlab.com/retry'; const retryURL = 'https://gitlab.com/retry';
beforeEach(() => { beforeEach(() => {
fixture.load('environments/element.html'); loadFixtures('static/environments/element.html.raw');
}); });
it('Should link to the provided retryUrl', () => { it('Should link to the provided retryUrl', () => {
......
//= require vue //= require vue
//= require environments/components/environment_stop //= require environments/components/environment_stop
describe('Stop Component', () => { describe('Stop Component', () => {
fixture.preload('environments/element.html'); preloadFixtures('static/environments/element.html.raw');
let stopURL; let stopURL;
let component; let component;
beforeEach(() => { beforeEach(() => {
fixture.load('environments/element.html'); loadFixtures('static/environments/element.html.raw');
stopURL = '/stop'; stopURL = '/stop';
component = new window.gl.environmentsList.StopComponent({ component = new window.gl.environmentsList.StopComponent({
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
describe('jQuery extensions', function() { describe('jQuery extensions', function() {
describe('disable', function() { describe('disable', function() {
beforeEach(function() { beforeEach(function() {
return fixture.set('<input type="text" />'); return setFixtures('<input type="text" />');
}); });
it('adds the disabled attribute', function() { it('adds the disabled attribute', function() {
var $input; var $input;
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
}); });
return describe('enable', function() { return describe('enable', function() {
beforeEach(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() { it('removes the disabled attribute', function() {
var $input; 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 @@ ...@@ -43,8 +43,7 @@
} }
describe('Dropdown', function describeDropdown() { describe('Dropdown', function describeDropdown() {
fixture.preload('gl_dropdown.html'); preloadFixtures('static/gl_dropdown.html.raw');
fixture.preload('projects.json');
function initDropDown(hasRemote, isFilterable) { function initDropDown(hasRemote, isFilterable) {
this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({ this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({
...@@ -61,10 +60,10 @@ ...@@ -61,10 +60,10 @@
} }
beforeEach(() => { beforeEach(() => {
fixture.load('gl_dropdown.html'); loadFixtures('static/gl_dropdown.html.raw');
this.dropdownContainerElement = $('.dropdown.inline'); this.dropdownContainerElement = $('.dropdown.inline');
this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement); this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
this.projectsData = fixture.load('projects.json')[0]; this.projectsData = getJSONFixture('projects.json');
}); });
afterEach(() => { afterEach(() => {
......
...@@ -4,11 +4,11 @@ ...@@ -4,11 +4,11 @@
//= require gl_field_errors //= require gl_field_errors
((global) => { ((global) => {
fixture.preload('gl_field_errors.html'); preloadFixtures('static/gl_field_errors.html.raw');
describe('GL Style Field Errors', function() { describe('GL Style Field Errors', function() {
beforeEach(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'); const $form = this.$form = $('form.gl-show-field-errors');
this.fieldErrors = new global.GlFieldErrors($form); this.fieldErrors = new global.GlFieldErrors($form);
}); });
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
describe('Header', function() { describe('Header', function() {
var todosPendingCount = '.todos-pending-count'; var todosPendingCount = '.todos-pending-count';
var fixtureTemplate = 'header.html'; var fixtureTemplate = 'static/header.html.raw';
function isTodosCountHidden() { function isTodosCountHidden() {
return $(todosPendingCount).hasClass('hidden'); return $(todosPendingCount).hasClass('hidden');
...@@ -17,9 +17,9 @@ ...@@ -17,9 +17,9 @@
$(document).trigger('todo:toggle', newCount); $(document).trigger('todo:toggle', newCount);
} }
fixture.preload(fixtureTemplate); preloadFixtures(fixtureTemplate);
beforeEach(function() { beforeEach(function() {
fixture.load(fixtureTemplate); loadFixtures(fixtureTemplate);
}); });
it('should update todos-pending-count after receiving the todo:toggle event', function() { it('should update todos-pending-count after receiving the todo:toggle event', function() {
......
...@@ -21,10 +21,10 @@ ...@@ -21,10 +21,10 @@
} }
describe('Issuable', () => { describe('Issuable', () => {
fixture.preload('issuable_filter'); preloadFixtures('static/issuable_filter.html.raw');
beforeEach(() => { beforeEach(() => {
fixture.load('issuable_filter'); loadFixtures('static/issuable_filter.html.raw');
Issuable.init(); Issuable.init();
}); });
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
beforeEach(() => { beforeEach(() => {
$filtersForm = $('.js-filter-form'); $filtersForm = $('.js-filter-form');
fixture.load('issuable_filter'); loadFixtures('static/issuable_filter.html.raw');
resetForm($filtersForm); resetForm($filtersForm);
}); });
......
...@@ -8,9 +8,9 @@ ...@@ -8,9 +8,9 @@
var INVALID_URL = 'http://goesnowhere.nothing/whereami'; var INVALID_URL = 'http://goesnowhere.nothing/whereami';
var $boxClosed, $boxOpen, $btnClose, $btnReopen; var $boxClosed, $boxOpen, $btnClose, $btnReopen;
fixture.preload('issues/closed-issue.html'); preloadFixtures('issues/closed-issue.html.raw');
fixture.preload('issues/issue-with-task-list.html'); preloadFixtures('issues/issue-with-task-list.html.raw');
fixture.preload('issues/open-issue.html'); preloadFixtures('issues/open-issue.html.raw');
function expectErrorMessage() { function expectErrorMessage() {
var $flashMessage = $('div.flash-alert'); var $flashMessage = $('div.flash-alert');
...@@ -61,8 +61,8 @@ ...@@ -61,8 +61,8 @@
describe('Issue', function() { describe('Issue', function() {
describe('task lists', function() { describe('task lists', function() {
fixture.load('issues/issue-with-task-list.html');
beforeEach(function() { beforeEach(function() {
loadFixtures('issues/issue-with-task-list.html.raw');
this.issue = new Issue(); this.issue = new Issue();
}); });
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
describe('close issue', function() { describe('close issue', function() {
beforeEach(function() { beforeEach(function() {
fixture.load('issues/open-issue.html'); loadFixtures('issues/open-issue.html.raw');
findElements(); findElements();
this.issue = new Issue(); this.issue = new Issue();
...@@ -140,7 +140,7 @@ ...@@ -140,7 +140,7 @@
describe('reopen issue', function() { describe('reopen issue', function() {
beforeEach(function() { beforeEach(function() {
fixture.load('issues/closed-issue.html'); loadFixtures('issues/closed-issue.html.raw');
findElements(); findElements();
this.issue = new Issue(); this.issue = new Issue();
......
...@@ -17,10 +17,10 @@ ...@@ -17,10 +17,10 @@
(() => { (() => {
let saveLabelCount = 0; let saveLabelCount = 0;
describe('Issue dropdown sidebar', () => { describe('Issue dropdown sidebar', () => {
fixture.preload('issue_sidebar_label.html'); preloadFixtures('static/issue_sidebar_label.html.raw');
beforeEach(() => { beforeEach(() => {
fixture.load('issue_sidebar_label.html'); loadFixtures('static/issue_sidebar_label.html.raw');
new IssuableContext('{"id":1,"name":"Administrator","username":"root"}'); new IssuableContext('{"id":1,"name":"Administrator","username":"root"}');
new LabelsSelect(); new LabelsSelect();
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
(function() { (function() {
describe('LineHighlighter', function() { describe('LineHighlighter', function() {
var clickLine; var clickLine;
fixture.preload('line_highlighter.html'); preloadFixtures('static/line_highlighter.html.raw');
clickLine = function(number, eventData) { clickLine = function(number, eventData) {
var e; var e;
if (eventData == null) { if (eventData == null) {
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
} }
}; };
beforeEach(function() { beforeEach(function() {
fixture.load('line_highlighter.html'); loadFixtures('static/line_highlighter.html.raw');
this["class"] = new LineHighlighter(); this["class"] = new LineHighlighter();
this.css = this["class"].highlightClass; this.css = this["class"].highlightClass;
return this.spies = { return this.spies = {
......
...@@ -6,9 +6,9 @@ ...@@ -6,9 +6,9 @@
(function() { (function() {
describe('MergeRequest', function() { describe('MergeRequest', function() {
return describe('task lists', function() { return describe('task lists', function() {
fixture.preload('merge_requests_show.html'); preloadFixtures('static/merge_requests_show.html.raw');
beforeEach(function() { beforeEach(function() {
fixture.load('merge_requests_show.html'); loadFixtures('static/merge_requests_show.html.raw');
return this.merge = new MergeRequest(); return this.merge = new MergeRequest();
}); });
it('modifies the Markdown field', function() { it('modifies the Markdown field', function() {
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
}; };
$.extend(stubLocation, defaults, stubs || {}); $.extend(stubLocation, defaults, stubs || {});
}; };
fixture.preload('merge_request_tabs.html'); preloadFixtures('static/merge_request_tabs.html.raw');
beforeEach(function () { beforeEach(function () {
this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation }); this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation });
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
describe('#activateTab', function () { describe('#activateTab', function () {
beforeEach(function () { beforeEach(function () {
spyOn($, 'ajax').and.callFake(function () {}); spyOn($, 'ajax').and.callFake(function () {});
fixture.load('merge_request_tabs.html'); loadFixtures('static/merge_request_tabs.html.raw');
this.subject = this.class.activateTab; this.subject = this.class.activateTab;
}); });
it('shows the first tab when action is show', function () { it('shows the first tab when action is show', function () {
......
...@@ -5,10 +5,10 @@ ...@@ -5,10 +5,10 @@
(() => { (() => {
describe('Mini Pipeline Graph Dropdown', () => { describe('Mini Pipeline Graph Dropdown', () => {
fixture.preload('mini_dropdown_graph'); preloadFixtures('static/mini_dropdown_graph.html.raw');
beforeEach(() => { beforeEach(() => {
fixture.load('mini_dropdown_graph'); loadFixtures('static/mini_dropdown_graph.html.raw');
}); });
describe('When is initialized', () => { describe('When is initialized', () => {
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
describe('Branch', function() { describe('Branch', function() {
return describe('create a new branch', function() { return describe('create a new branch', function() {
var expectToHaveError, fillNameWith; var expectToHaveError, fillNameWith;
fixture.preload('new_branch.html'); preloadFixtures('static/new_branch.html.raw');
fillNameWith = function(value) { fillNameWith = function(value) {
return $('.js-branch-name').val(value).trigger('blur'); return $('.js-branch-name').val(value).trigger('blur');
}; };
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
return expect($('.js-branch-name-error span').text()).toEqual(error); return expect($('.js-branch-name-error span').text()).toEqual(error);
}; };
beforeEach(function() { beforeEach(function() {
fixture.load('new_branch.html'); loadFixtures('static/new_branch.html.raw');
$('form').on('submit', function(e) { $('form').on('submit', function(e) {
return e.preventDefault(); return e.preventDefault();
}); });
......
...@@ -12,11 +12,11 @@ ...@@ -12,11 +12,11 @@
gl.utils = gl.utils || {}; gl.utils = gl.utils || {};
describe('Notes', function() { describe('Notes', function() {
var commentsTemplate = 'issues/issue_with_comment.raw'; var commentsTemplate = 'issues/issue_with_comment.html.raw';
fixture.preload(commentsTemplate); preloadFixtures(commentsTemplate);
beforeEach(function () { beforeEach(function () {
fixture.load(commentsTemplate); loadFixtures(commentsTemplate);
gl.utils.disableButtonIfEmptyField = _.noop; gl.utils.disableButtonIfEmptyField = _.noop;
window.project_uploads_path = 'http://test.host/uploads'; window.project_uploads_path = 'http://test.host/uploads';
$('body').data('page', 'projects:issues:show'); $('body').data('page', 'projects:issues:show');
......
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
(() => { (() => {
describe('Pipelines', () => { describe('Pipelines', () => {
fixture.preload('pipeline_graph'); preloadFixtures('static/pipeline_graph.html.raw');
beforeEach(() => { beforeEach(() => {
fixture.load('pipeline_graph'); loadFixtures('static/pipeline_graph.html.raw');
}); });
it('should be defined', () => { it('should be defined', () => {
......
...@@ -16,10 +16,9 @@ ...@@ -16,10 +16,9 @@
window.gon.api_version = 'v3'; window.gon.api_version = 'v3';
describe('Project Title', function() { describe('Project Title', function() {
fixture.preload('project_title.html'); preloadFixtures('static/project_title.html.raw');
fixture.preload('projects.json');
beforeEach(function() { beforeEach(function() {
fixture.load('project_title.html'); loadFixtures('static/project_title.html.raw');
return this.project = new Project(); return this.project = new Project();
}); });
return describe('project list', function() { return describe('project list', function() {
...@@ -34,7 +33,7 @@ ...@@ -34,7 +33,7 @@
beforeEach((function(_this) { beforeEach((function(_this) {
return function() { 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)); return spyOn(jQuery, 'ajax').and.callFake(fakeAjaxResponse.bind(_this));
}; };
})(this)); })(this));
......
...@@ -36,9 +36,9 @@ ...@@ -36,9 +36,9 @@
describe('RightSidebar', function() { describe('RightSidebar', function() {
var fixtureName = 'issues/open-issue.html.raw'; var fixtureName = 'issues/open-issue.html.raw';
fixture.preload(fixtureName); preloadFixtures(fixtureName);
beforeEach(function() { beforeEach(function() {
fixture.load(fixtureName); loadFixtures(fixtureName);
this.sidebar = new Sidebar; this.sidebar = new Sidebar;
$aside = $('.right-sidebar'); $aside = $('.right-sidebar');
$page = $('.page-with-sidebar'); $page = $('.page-with-sidebar');
...@@ -65,9 +65,10 @@ ...@@ -65,9 +65,10 @@
}); });
it('should broadcast todo:toggle event when add todo clicked', function() { it('should broadcast todo:toggle event when add todo clicked', function() {
var todos = getJSONFixture('todos.json');
spyOn(jQuery, 'ajax').and.callFake(function() { spyOn(jQuery, 'ajax').and.callFake(function() {
var d = $.Deferred(); var d = $.Deferred();
var response = fixture.load('todos.json'); var response = todos;
d.resolve(response); d.resolve(response);
return d.promise(); return d.promise();
}); });
......
...@@ -112,9 +112,9 @@ ...@@ -112,9 +112,9 @@
}; };
describe('Search autocomplete dropdown', function() { describe('Search autocomplete dropdown', function() {
fixture.preload('search_autocomplete.html'); preloadFixtures('static/search_autocomplete.html.raw');
beforeEach(function() { beforeEach(function() {
fixture.load('search_autocomplete.html'); loadFixtures('static/search_autocomplete.html.raw');
return widget = new gl.SearchAutocomplete; return widget = new gl.SearchAutocomplete;
}); });
it('should show Dashboard specific dropdown menu', function() { it('should show Dashboard specific dropdown menu', function() {
......
...@@ -6,9 +6,9 @@ ...@@ -6,9 +6,9 @@
(function() { (function() {
describe('ShortcutsIssuable', function() { describe('ShortcutsIssuable', function() {
var fixtureName = 'issues/open-issue.html.raw'; var fixtureName = 'issues/open-issue.html.raw';
fixture.preload(fixtureName); preloadFixtures(fixtureName);
beforeEach(function() { beforeEach(function() {
fixture.load(fixtureName); loadFixtures(fixtureName);
document.querySelector('.js-new-note-form').classList.add('js-main-target-form'); document.querySelector('.js-new-note-form').classList.add('js-main-target-form');
return this.shortcut = new ShortcutsIssuable(); return this.shortcut = new ShortcutsIssuable();
}); });
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
((global) => { ((global) => {
describe('SigninTabsMemoizer', () => { describe('SigninTabsMemoizer', () => {
const fixtureTemplate = 'signin_tabs.html'; const fixtureTemplate = 'static/signin_tabs.html.raw';
const tabSelector = 'ul.nav-tabs'; const tabSelector = 'ul.nav-tabs';
const currentTabKey = 'current_signin_tab'; const currentTabKey = 'current_signin_tab';
let memo; let memo;
...@@ -15,10 +15,10 @@ ...@@ -15,10 +15,10 @@
return memo; return memo;
} }
fixture.preload(fixtureTemplate); preloadFixtures(fixtureTemplate);
beforeEach(() => { beforeEach(() => {
fixture.load(fixtureTemplate); loadFixtures(fixtureTemplate);
}); });
it('does nothing if no tab was previously selected', () => { it('does nothing if no tab was previously selected', () => {
......
...@@ -103,7 +103,7 @@ ...@@ -103,7 +103,7 @@
describe('DOM Events', function () { describe('DOM Events', function () {
beforeEach(function () { beforeEach(function () {
// This ensures DOM and DOM events are initialized for these specs. // This ensures DOM and DOM events are initialized for these specs.
fixture.set('<div></div>'); setFixtures('<div></div>');
this.smartInterval = createDefaultSmartInterval(); this.smartInterval = createDefaultSmartInterval();
}); });
......
...@@ -37,12 +37,12 @@ ...@@ -37,12 +37,12 @@
// file as a manifest. // file as a manifest.
// For more information: http://github.com/modeset/teaspoon // For more information: http://github.com/modeset/teaspoon
(function() { // set our fixtures path
jasmine.getFixtures().fixturesPath = '/teaspoon/fixtures';
jasmine.getJSONFixtures().fixturesPath = '/teaspoon/fixtures';
}).call(this);
// defined in ActionDispatch::TestRequest // defined in ActionDispatch::TestRequest
// see https://github.com/rails/rails/blob/v4.2.7.1/actionpack/lib/action_dispatch/testing/test_request.rb#L7 // see https://github.com/rails/rails/blob/v4.2.7.1/actionpack/lib/action_dispatch/testing/test_request.rb#L7
window.gl = window.gl || {}; 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 @@ ...@@ -13,7 +13,7 @@
}; };
describe('on a js-syntax-highlight element', function() { describe('on a js-syntax-highlight element', function() {
beforeEach(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() { return it('applies syntax highlighting', function() {
stubUserColorScheme('monokai'); stubUserColorScheme('monokai');
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
}); });
return describe('on a parent element', function() { return describe('on a parent element', function() {
beforeEach(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() { it('applies highlighting to all applicable children', function() {
stubUserColorScheme('monokai'); stubUserColorScheme('monokai');
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
}); });
return it('prevents an infinite loop when no matches exist', function() { return it('prevents an infinite loop when no matches exist', function() {
var highlight; var highlight;
fixture.set('<div></div>'); setFixtures('<div></div>');
highlight = function() { highlight = function() {
return $('div').syntaxHighlight(); return $('div').syntaxHighlight();
}; };
......
...@@ -10,8 +10,10 @@ ...@@ -10,8 +10,10 @@
(function() { (function() {
describe('U2FAuthenticate', function() { describe('U2FAuthenticate', function() {
fixture.load('u2f/authenticate'); preloadFixtures('u2f/authenticate.html.raw');
beforeEach(function() { beforeEach(function() {
loadFixtures('u2f/authenticate.html.raw');
this.u2fDevice = new MockU2FDevice; this.u2fDevice = new MockU2FDevice;
this.container = $("#js-authenticate-u2f"); this.container = $("#js-authenticate-u2f");
this.component = new window.gl.U2FAuthenticate( this.component = new window.gl.U2FAuthenticate(
......
...@@ -10,8 +10,10 @@ ...@@ -10,8 +10,10 @@
(function() { (function() {
describe('U2FRegister', function() { describe('U2FRegister', function() {
fixture.load('u2f/register'); preloadFixtures('u2f/register.html.raw');
beforeEach(function() { beforeEach(function() {
loadFixtures('u2f/register.html.raw');
this.u2fDevice = new MockU2FDevice; this.u2fDevice = new MockU2FDevice;
this.container = $("#js-register-u2f"); this.container = $("#js-register-u2f");
this.component = new U2FRegister(this.container, $("#js-register-u2f-templates"), {}, "token"); this.component = new U2FRegister(this.container, $("#js-register-u2f-templates"), {}, "token");
......
...@@ -5,7 +5,7 @@ describe('Commit component', () => { ...@@ -5,7 +5,7 @@ describe('Commit component', () => {
let component; let component;
it('should render a code-fork icon if it does not represent a tag', () => { 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({ component = new window.gl.CommitComponent({
el: document.querySelector('.test-commit-container'), el: document.querySelector('.test-commit-container'),
propsData: { propsData: {
...@@ -30,7 +30,7 @@ describe('Commit component', () => { ...@@ -30,7 +30,7 @@ describe('Commit component', () => {
describe('Given all the props', () => { describe('Given all the props', () => {
beforeEach(() => { beforeEach(() => {
fixture.set('<div class="test-commit-container"></div>'); setFixtures('<div class="test-commit-container"></div>');
props = { props = {
tag: true, tag: true,
...@@ -105,7 +105,7 @@ describe('Commit component', () => { ...@@ -105,7 +105,7 @@ describe('Commit component', () => {
describe('When commit title is not provided', () => { describe('When commit title is not provided', () => {
it('should render default message', () => { it('should render default message', () => {
fixture.set('<div class="test-commit-container"></div>'); setFixtures('<div class="test-commit-container"></div>');
props = { props = {
tag: false, tag: false,
commitRef: { commitRef: {
......
...@@ -10,9 +10,9 @@ ...@@ -10,9 +10,9 @@
describe('ZenMode', function() { describe('ZenMode', function() {
var fixtureName = 'issues/open-issue.html.raw'; var fixtureName = 'issues/open-issue.html.raw';
fixture.preload(fixtureName); preloadFixtures(fixtureName);
beforeEach(function() { beforeEach(function() {
fixture.load(fixtureName); loadFixtures(fixtureName);
spyOn(Dropzone, 'forElement').and.callFake(function() { spyOn(Dropzone, 'forElement').and.callFake(function() {
return { return {
enable: function() { enable: function() {
......
...@@ -136,6 +136,14 @@ describe Ci::Ansi2html, lib: true do ...@@ -136,6 +136,14 @@ describe Ci::Ansi2html, lib: true do
expect(subject.convert("<")[:html]).to eq('&lt;') expect(subject.convert("<")[:html]).to eq('&lt;')
end 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 describe "incremental update" do
shared_examples 'stateable converter' do shared_examples 'stateable converter' do
let(:pass1) { subject.convert(pre_text) } let(:pass1) { subject.convert(pre_text) }
......
...@@ -248,6 +248,7 @@ DeployKey: ...@@ -248,6 +248,7 @@ DeployKey:
- fingerprint - fingerprint
- public - public
- can_push - can_push
- last_used_at
Service: Service:
- id - id
- type - type
......
...@@ -15,9 +15,9 @@ describe Gitlab::LDAP::Access, lib: true do ...@@ -15,9 +15,9 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
it 'should block user in GitLab' do it 'should block user in GitLab' do
expect(access).to receive(:block_user).with(user, 'does not exist anymore')
access.allowed? access.allowed?
expect(user).to be_blocked
expect(user).to be_ldap_blocked
end end
end end
...@@ -34,9 +34,9 @@ describe Gitlab::LDAP::Access, lib: true do ...@@ -34,9 +34,9 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
it 'blocks user in GitLab' do it 'blocks user in GitLab' do
expect(access).to receive(:block_user).with(user, 'is disabled in Active Directory')
access.allowed? access.allowed?
expect(user).to be_blocked
expect(user).to be_ldap_blocked
end end
end end
...@@ -53,7 +53,10 @@ describe Gitlab::LDAP::Access, lib: true do ...@@ -53,7 +53,10 @@ describe Gitlab::LDAP::Access, lib: true do
end end
it 'does not unblock user in GitLab' do it 'does not unblock user in GitLab' do
expect(access).not_to receive(:unblock_user)
access.allowed? access.allowed?
expect(user).to be_blocked expect(user).to be_blocked
expect(user).not_to be_ldap_blocked # this block is handled by omniauth not by our internal logic expect(user).not_to be_ldap_blocked # this block is handled by omniauth not by our internal logic
end end
...@@ -65,8 +68,9 @@ describe Gitlab::LDAP::Access, lib: true do ...@@ -65,8 +68,9 @@ describe Gitlab::LDAP::Access, lib: true do
end end
it 'unblocks user in GitLab' do it 'unblocks user in GitLab' do
expect(access).to receive(:unblock_user).with(user, 'is not disabled anymore')
access.allowed? access.allowed?
expect(user).not_to be_blocked
end end
end end
end end
...@@ -87,9 +91,9 @@ describe Gitlab::LDAP::Access, lib: true do ...@@ -87,9 +91,9 @@ describe Gitlab::LDAP::Access, lib: true do
it { is_expected.to be_falsey } it { is_expected.to be_falsey }
it 'blocks user in GitLab' do it 'blocks user in GitLab' do
expect(access).to receive(:block_user).with(user, 'does not exist anymore')
access.allowed? access.allowed?
expect(user).to be_blocked
expect(user).to be_ldap_blocked
end end
end end
...@@ -99,11 +103,54 @@ describe Gitlab::LDAP::Access, lib: true do ...@@ -99,11 +103,54 @@ describe Gitlab::LDAP::Access, lib: true do
end end
it 'unblocks the user if it exists' do it 'unblocks the user if it exists' do
expect(access).to receive(:unblock_user).with(user, 'is available again')
access.allowed? access.allowed?
expect(user).not_to be_blocked
end end
end end
end 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 end
...@@ -129,4 +129,27 @@ describe Gitlab::LDAP::Config, lib: true do ...@@ -129,4 +129,27 @@ describe Gitlab::LDAP::Config, lib: true do
expect(config.has_auth?).to be_falsey expect(config.has_auth?).to be_falsey
end end
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 end
...@@ -7,9 +7,11 @@ describe Gitlab::LDAP::Person do ...@@ -7,9 +7,11 @@ describe Gitlab::LDAP::Person do
before do before do
stub_ldap_config( stub_ldap_config(
attributes: { options: {
name: 'cn', 'attributes' => {
email: %w(mail email userPrincipalName) 'name' => 'cn',
'email' => %w(mail email userPrincipalName)
}
} }
) )
end end
...@@ -30,7 +32,7 @@ describe Gitlab::LDAP::Person do ...@@ -30,7 +32,7 @@ describe Gitlab::LDAP::Person do
entry['mail'] = mail entry['mail'] = mail
person = Gitlab::LDAP::Person.new(entry, 'ldapmain') person = Gitlab::LDAP::Person.new(entry, 'ldapmain')
expect(person.email).to eq(mail) expect(person.email).to eq([mail])
end end
it 'returns the value of userPrincipalName, if mail and email are not present' do it 'returns the value of userPrincipalName, if mail and email are not present' do
...@@ -38,7 +40,7 @@ describe Gitlab::LDAP::Person do ...@@ -38,7 +40,7 @@ describe Gitlab::LDAP::Person do
entry['userPrincipalName'] = user_principal_name entry['userPrincipalName'] = user_principal_name
person = Gitlab::LDAP::Person.new(entry, 'ldapmain') 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 end
end end
...@@ -7,26 +7,72 @@ describe GlobalMilestone, models: true do ...@@ -7,26 +7,72 @@ describe GlobalMilestone, models: true do
let(:project1) { create(:project, group: group) } let(:project1) { create(:project, group: group) }
let(:project2) { create(:project, path: 'gitlab-ci', group: group) } let(:project2) { create(:project, path: 'gitlab-ci', group: group) }
let(:project3) { create(:project, path: 'cookbook-gitlab', 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 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 before do
milestones = projects = [
[ project1,
milestone1_project1, project2,
milestone1_project2, project3
milestone1_project3,
milestone2_project1,
milestone2_project2,
milestone2_project3
] ]
@global_milestones = GlobalMilestone.build_collection(milestones) @global_milestones = GlobalMilestone.build_collection(projects, {})
end end
it 'has all project milestones' do it 'has all project milestones' do
...@@ -40,9 +86,17 @@ describe GlobalMilestone, models: true do ...@@ -40,9 +86,17 @@ describe GlobalMilestone, models: true do
it 'has all project milestones' do it 'has all project milestones' do
expect(@global_milestones.map { |group_milestone| group_milestone.milestones.count }.sum).to eq(6) expect(@global_milestones.map { |group_milestone| group_milestone.milestones.count }.sum).to eq(6)
end end
it 'sorts collection by due date' do
expect(@global_milestones.map(&:due_date)).to eq [nil, milestone1_due_date]
end
end end
describe '#initialize' do 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 before do
milestones = 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 ...@@ -28,6 +28,15 @@ describe Key, models: true do
expect(build(:key, user: user).publishable_key).to include("#{user.name} (#{Gitlab.config.gitlab.host})") expect(build(:key, user: user).publishable_key).to include("#{user.name} (#{Gitlab.config.gitlab.host})")
end end
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 end
context "validation of uniqueness (based on fingerprint uniqueness)" do context "validation of uniqueness (based on fingerprint uniqueness)" do
......
...@@ -265,6 +265,14 @@ describe API::Users, api: true do ...@@ -265,6 +265,14 @@ describe API::Users, api: true do
expect(response).to have_http_status(409) expect(response).to have_http_status(409)
expect(json_response['message']).to eq('Username has already been taken') expect(json_response['message']).to eq('Username has already been taken')
end 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
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 ...@@ -20,12 +20,26 @@ module JavaScriptFixturesHelpers
# Public: Store a response object as fixture file # 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) # fixture_file_name - file name to store the fixture in (relative to FIXTURE_PATH)
# #
def store_frontend_fixture(response, fixture_file_name) def store_frontend_fixture(response, fixture_file_name)
fixture_file_name = File.expand_path(fixture_file_name, FIXTURE_PATH) 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 = response.body
fixture.force_encoding("utf-8")
response_mime_type = Mime::Type.lookup(response.content_type) response_mime_type = Mime::Type.lookup(response.content_type)
if response_mime_type.html? if response_mime_type.html?
...@@ -34,7 +48,7 @@ module JavaScriptFixturesHelpers ...@@ -34,7 +48,7 @@ module JavaScriptFixturesHelpers
link_tags = doc.css('link') link_tags = doc.css('link')
link_tags.remove link_tags.remove
scripts = doc.css('script') scripts = doc.css("script:not([type='text/template'])")
scripts.remove scripts.remove
fixture = doc.to_html fixture = doc.to_html
...@@ -44,7 +58,6 @@ module JavaScriptFixturesHelpers ...@@ -44,7 +58,6 @@ module JavaScriptFixturesHelpers
fixture.gsub!(%r{="/}, "=\"http://#{test_host}/") fixture.gsub!(%r{="/}, "=\"http://#{test_host}/")
end end
FileUtils.mkdir_p(File.dirname(fixture_file_name)) fixture
File.write(fixture_file_name, fixture)
end end
end end
...@@ -7,6 +7,7 @@ describe 'projects/merge_requests/show.html.haml' do ...@@ -7,6 +7,7 @@ describe 'projects/merge_requests/show.html.haml' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:fork_project) { create(:project, forked_from_project: project) } let(:fork_project) { create(:project, forked_from_project: project) }
let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } 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 let(:closed_merge_request) do
create(:closed_merge_request, create(:closed_merge_request,
...@@ -19,8 +20,12 @@ describe 'projects/merge_requests/show.html.haml' do ...@@ -19,8 +20,12 @@ describe 'projects/merge_requests/show.html.haml' do
assign(:project, project) assign(:project, project)
assign(:merge_request, closed_merge_request) assign(:merge_request, closed_merge_request)
assign(:commits_count, 0) 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 end
context 'when the merge request is closed' do 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