Commit 44558c72 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge branch 'master' into all-skipped-equals-success

* master: (103 commits)
  Fixes sidebar navigation.
  Convert "SSH Keys" Spinach features to RSpec
  Enable import/export back for non-admins
  Update gitlab-shell to 3.6.3
  Updated artwork of empty group state.
  Better empty state for Groups view.
  Members::RequestAccessService is tricter on permissions
  Add a /wip slash command
  Link to the "What requires downtime?" page from the Migration Style Guide
  Fix RuboCop failure in app/services/notification_service.rb
  Add word-wrap to issue title on issue and milestone boards
  Fix page scrolling to top on sidebar toggle
  Changed zero padded days to no padded days in date_format
  GrapeDSL for Keys endpoint
  Remove duplicate test
  Add a spec to verify comparison context inclusion in path when a version is chosen to compare against
  Add flash containers and broadcast messages below subnav
  Add white background to create MR banner
  Move create MR banner below subnav
  Remove contianer from last push widget
  ...
parents afc0ae5c 8c5701b6
image: "ruby:2.3.1" image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3-git-2.7-phantomjs-2.1"
cache: cache:
key: "ruby-231" key: "ruby-231"
paths: paths:
- vendor/apt
- vendor/ruby - vendor/ruby
variables: variables:
...@@ -141,14 +140,13 @@ spinach 9 10: *spinach-knapsack ...@@ -141,14 +140,13 @@ spinach 9 10: *spinach-knapsack
# Execute all testing suites against Ruby 2.1 # Execute all testing suites against Ruby 2.1
.ruby-21: &ruby-21 .ruby-21: &ruby-21
image: "ruby:2.1" image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.1-git-2.7-phantomjs-2.1"
<<: *use-db <<: *use-db
only: only:
- master - master
cache: cache:
key: "ruby21" key: "ruby21"
paths: paths:
- vendor/apt
- vendor/ruby - vendor/ruby
.rspec-knapsack-ruby21: &rspec-knapsack-ruby21 .rspec-knapsack-ruby21: &rspec-knapsack-ruby21
......
Please view this file on the master branch, on stable branches it's out of date. Please view this file on the master branch, on stable branches it's out of date.
v 8.13.0 (unreleased) v 8.13.0 (unreleased)
- Add link from system note to compare with previous version
- Use gitlab-shell v3.6.2 (GIT TRACE logging) - Use gitlab-shell v3.6.2 (GIT TRACE logging)
- Fix centering of custom header logos (Ashley Dumaine)
- AbstractReferenceFilter caches project_refs on RequestStore when active - AbstractReferenceFilter caches project_refs on RequestStore when active
- Replaced the check sign to arrow in the show build view. !6501
- Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar)
- Speed-up group milestones show page - Speed-up group milestones show page
- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller) - Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
- Add more tests for calendar contribution (ClemMakesApps) - Add more tests for calendar contribution (ClemMakesApps)
- Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references - Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
- Fix permission for setting an issue's due date - Fix permission for setting an issue's due date
- Expose expires_at field when sharing project on API - Expose expires_at field when sharing project on API
- Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
- Allow the Koding integration to be configured through the API
- Added soft wrap button to repository file/blob editor
- Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
- Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison) - Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
- Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska)
- Use a ConnectionPool for Rails.cache on Sidekiq servers - Use a ConnectionPool for Rails.cache on Sidekiq servers
- Replace `alias_method_chain` with `Module#prepend`
- Enable GitLab Import/Export for non-admin users.
- Preserve label filters when sorting !6136 (Joseph Frazier)
- Only update issuable labels if they have been changed - Only update issuable labels if they have been changed
- Take filters in account in issuable counters. !6496
- Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*)
- Revoke button in Applications Settings underlines on hover. - Revoke button in Applications Settings underlines on hover.
- Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
- Fix Long commit messages overflow viewport in file tree - Fix Long commit messages overflow viewport in file tree
- Revert avoid touching file system on Build#artifacts?
- Add broadcast messages and alerts below sub-nav
- Better empty state for Groups view
- Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe) - Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe)
- Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
- Add organization field to user profile - Add organization field to user profile
- Fix resolved discussion display in side-by-side diff view !6575 - Fix resolved discussion display in side-by-side diff view !6575
- Optimize GitHub importing for speed and memory - Optimize GitHub importing for speed and memory
- API: expose pipeline data in builds API (!6502, Guilherme Salazar) - API: expose pipeline data in builds API (!6502, Guilherme Salazar)
- Notify the Merger about merge after successful build (Dimitris Karakasilis)
- Fix broken repository 500 errors in project list - Fix broken repository 500 errors in project list
- Close todos when accepting merge requests via the API !6486 (tonygambone)
v 8.12.4 (unreleased)
v 8.12.3
- Update Gitlab Shell to support low IO priority for storage moves
v 8.12.2 (unreleased) v 8.12.2 (unreleased)
- Added University content to doc/university
- Fix Import/Export not recognising correctly the imported services. - Fix Import/Export not recognising correctly the imported services.
- Fix snippets pagination - Fix snippets pagination
- Fix "Create project" button layout when visibility options are restricted
- Fix List-Unsubscribe header in emails - Fix List-Unsubscribe header in emails
- Fix IssuesController#show degradation including project on loaded notes - Fix IssuesController#show degradation including project on loaded notes
- Fix an issue with the "Commits" section of the cycle analytics summary. !6513 - Fix an issue with the "Commits" section of the cycle analytics summary. !6513
- Fix errors importing project feature and milestone models using GitLab project import - Fix errors importing project feature and milestone models using GitLab project import
- Make JWT messages Docker-compatible - Make JWT messages Docker-compatible
- Fix duplicate branch entry in the merge request version compare dropdown - Fix duplicate branch entry in the merge request version compare dropdown
- Respect the fork_project permission when forking projects
- Only update issuable labels if they have been changed
- Fix bug where 'Search results' repeated many times when a search in the emoji search form is cleared (Xavier Bick) (@zeiv)
- Fix resolve discussion buttons endpoint path
v 8.12.1 v 8.12.1
- Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST - Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST
- Fix issue with search filter labels not displaying - Fix issue with search filter labels not displaying
- Fix bug where 'Search results' repeated many times when a search in the emoji search form is cleared (Xavier Bick) (@zeiv)
v 8.12.0 v 8.12.0
- Update the rouge gem to 2.0.6, which adds highlighting support for JSX, Prometheus, and others. !6251 - Update the rouge gem to 2.0.6, which adds highlighting support for JSX, Prometheus, and others. !6251
...@@ -52,7 +81,6 @@ v 8.12.0 ...@@ -52,7 +81,6 @@ v 8.12.0
- Filter tags by name !6121 - Filter tags by name !6121
- Update gitlab shell secret file also when it is empty. !3774 (glensc) - Update gitlab shell secret file also when it is empty. !3774 (glensc)
- Give project selection dropdowns responsive width, make non-wrapping. - Give project selection dropdowns responsive width, make non-wrapping.
- Fix resolve discussion buttons endpoint path
- Fix note form hint showing slash commands supported for commits. - Fix note form hint showing slash commands supported for commits.
- Make push events have equal vertical spacing. - Make push events have equal vertical spacing.
- API: Ensure invitees are not returned in Members API. - API: Ensure invitees are not returned in Members API.
......
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
// submitted textarea // submitted textarea
})(this)); })(this));
this.initModePanesAndLinks(); this.initModePanesAndLinks();
this.initSoftWrap();
new BlobLicenseSelectors({ new BlobLicenseSelectors({
editor: this.editor editor: this.editor
}); });
...@@ -50,6 +51,7 @@ ...@@ -50,6 +51,7 @@
this.$editModePanes.hide(); this.$editModePanes.hide();
currentPane.fadeIn(200); currentPane.fadeIn(200);
if (paneId === "#preview") { if (paneId === "#preview") {
this.$toggleButton.hide();
return $.post(currentLink.data("preview-url"), { return $.post(currentLink.data("preview-url"), {
content: this.editor.getValue() content: this.editor.getValue()
}, function(response) { }, function(response) {
...@@ -57,10 +59,23 @@ ...@@ -57,10 +59,23 @@
return currentPane.syntaxHighlight(); return currentPane.syntaxHighlight();
}); });
} else { } else {
this.$toggleButton.show();
return this.editor.focus(); return this.editor.focus();
} }
}; };
EditBlob.prototype.initSoftWrap = function() {
this.isSoftWrapped = false;
this.$toggleButton = $('.soft-wrap-toggle');
this.$toggleButton.on('click', this.toggleSoftWrap.bind(this));
};
EditBlob.prototype.toggleSoftWrap = function(e) {
this.isSoftWrapped = !this.isSoftWrapped;
this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped);
this.editor.getSession().setUseWrapMode(this.isSoftWrapped);
};
return EditBlob; return EditBlob;
})(); })();
......
...@@ -23,8 +23,9 @@ ...@@ -23,8 +23,9 @@
selectable: true, selectable: true,
filterable: true, filterable: true,
filterByText: true, filterByText: true,
fieldName: $dropdown.attr('name'), toggleLabel: true,
filterInput: 'input[type="text"]', fieldName: $dropdown.data('field-name'),
filterInput: 'input[type="search"]',
renderRow: function(ref) { renderRow: function(ref) {
var link; var link;
if (ref.header != null) { if (ref.header != null) {
......
...@@ -10,12 +10,13 @@ ...@@ -10,12 +10,13 @@
ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>'; ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>';
COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. Click to expand it.</div>'; COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>';
function SingleFileDiff(file) { function SingleFileDiff(file) {
this.file = file; this.file = file;
this.toggleDiff = bind(this.toggleDiff, this); this.toggleDiff = bind(this.toggleDiff, this);
this.content = $('.diff-content', this.file); this.content = $('.diff-content', this.file);
this.$toggleIcon = $('.diff-toggle-caret', this.file);
this.diffForPath = this.content.find('[data-diff-for-path]').data('diff-for-path'); this.diffForPath = this.content.find('[data-diff-for-path]').data('diff-for-path');
this.isOpen = !this.diffForPath; this.isOpen = !this.diffForPath;
if (this.diffForPath) { if (this.diffForPath) {
...@@ -23,18 +24,22 @@ ...@@ -23,18 +24,22 @@
this.loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide(); this.loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide();
this.content = null; this.content = null;
this.collapsedContent.after(this.loadingContent); this.collapsedContent.after(this.loadingContent);
this.$toggleIcon.addClass('fa-caret-right');
} else { } else {
this.collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide(); this.collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide();
this.content.after(this.collapsedContent); this.content.after(this.collapsedContent);
this.$toggleIcon.addClass('fa-caret-down');
} }
this.collapsedContent.on('click', this.toggleDiff); $('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff);
$('.file-title > a', this.file).on('click', this.toggleDiff);
} }
SingleFileDiff.prototype.toggleDiff = function(e) { SingleFileDiff.prototype.toggleDiff = function(e) {
var $target = $(e.target);
if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
this.isOpen = !this.isOpen; this.isOpen = !this.isOpen;
if (!this.isOpen && !this.hasError) { if (!this.isOpen && !this.hasError) {
this.content.hide(); this.content.hide();
this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down');
this.collapsedContent.show(); this.collapsedContent.show();
if (typeof DiffNotesApp !== 'undefined') { if (typeof DiffNotesApp !== 'undefined') {
DiffNotesApp.compileComponents(); DiffNotesApp.compileComponents();
...@@ -42,10 +47,12 @@ ...@@ -42,10 +47,12 @@
} else if (this.content) { } else if (this.content) {
this.collapsedContent.hide(); this.collapsedContent.hide();
this.content.show(); this.content.show();
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
if (typeof DiffNotesApp !== 'undefined') { if (typeof DiffNotesApp !== 'undefined') {
DiffNotesApp.compileComponents(); DiffNotesApp.compileComponents();
} }
} else { } else {
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
return this.getContentHTML(); return this.getContentHTML();
} }
}; };
......
...@@ -19,10 +19,8 @@ ...@@ -19,10 +19,8 @@
&.diff-collapsed { &.diff-collapsed {
padding: 5px; padding: 5px;
cursor: pointer; .click-to-expand {
cursor: pointer;
&:hover {
background-color: $row-hover;
} }
} }
} }
......
...@@ -26,6 +26,15 @@ ...@@ -26,6 +26,15 @@
padding: 10px $gl-padding; padding: 10px $gl-padding;
word-wrap: break-word; word-wrap: break-word;
border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0;
cursor: pointer;
&:hover {
background-color: $dark-background-color;
}
.diff-toggle-caret {
padding-right: 6px;
}
&.file-title-clear { &.file-title-clear {
padding-left: 0; padding-left: 0;
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
margin: 0; margin: 0;
margin-bottom: $gl-padding; margin-bottom: $gl-padding;
font-size: 14px; font-size: 14px;
position: relative;
z-index: 1;
.flash-notice { .flash-notice {
@extend .alert; @extend .alert;
...@@ -33,6 +35,12 @@ ...@@ -33,6 +35,12 @@
} }
} }
.content-wrapper {
.flash-notice .container-fluid {
background-color: transparent;
}
}
@media (max-width: $screen-md-min) { @media (max-width: $screen-md-min) {
ul.notes { ul.notes {
.flash-container.timeline-content { .flash-container.timeline-content {
......
...@@ -112,11 +112,15 @@ header { ...@@ -112,11 +112,15 @@ header {
.header-logo { .header-logo {
position: absolute; position: absolute;
left: 50%; left: 50%;
margin-left: -18px;
top: 7px; top: 7px;
transition-duration: .3s; transition-duration: .3s;
z-index: 999; z-index: 999;
#logo {
position: relative;
left: -50%;
}
svg, img { svg, img {
height: 36px; height: 36px;
} }
...@@ -126,8 +130,12 @@ header { ...@@ -126,8 +130,12 @@ header {
} }
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
right: 25px; right: 20px;
left: auto; left: auto;
#logo {
left: auto;
}
} }
} }
......
...@@ -142,6 +142,7 @@ ...@@ -142,6 +142,7 @@
transition-duration: .3s; transition-duration: .3s;
position: absolute; position: absolute;
top: 0; top: 0;
cursor: pointer;
&:hover, &:hover,
&:focus { &:focus {
......
...@@ -197,6 +197,7 @@ lex ...@@ -197,6 +197,7 @@ lex
a { a {
color: inherit; color: inherit;
word-wrap: break-word;
} }
} }
......
...@@ -107,10 +107,14 @@ ...@@ -107,10 +107,14 @@
.block { .block {
width: 100%; width: 100%;
}
.block-first { &.coverage {
padding: 5px 16px 11px; padding: 0 16px 11px;
}
.btn-group-justified {
margin-top: 5px;
}
} }
.js-build-variable { .js-build-variable {
...@@ -214,6 +218,9 @@ ...@@ -214,6 +218,9 @@
.build-detail-row { .build-detail-row {
margin-bottom: 5px; margin-bottom: 5px;
&:last-of-type {
margin-bottom: 0;
}
} }
.build-light-text { .build-light-text {
......
...@@ -59,6 +59,7 @@ ...@@ -59,6 +59,7 @@
} }
.encoding-selector, .encoding-selector,
.soft-wrap-toggle,
.license-selector, .license-selector,
.gitignore-selector, .gitignore-selector,
.gitlab-ci-yml-selector { .gitlab-ci-yml-selector {
...@@ -67,6 +68,24 @@ ...@@ -67,6 +68,24 @@
font-family: $regular_font; font-family: $regular_font;
} }
.soft-wrap-toggle {
margin: 0 $btn-side-margin;
.soft-wrap {
display: block;
}
.no-wrap {
display: none;
}
&.soft-wrap-active {
.soft-wrap {
display: none;
}
.no-wrap {
display: block;
}
}
}
.gitignore-selector, .license-selector, .gitlab-ci-yml-selector { .gitignore-selector, .license-selector, .gitlab-ci-yml-selector {
.dropdown { .dropdown {
line-height: 21px; line-height: 21px;
......
...@@ -57,7 +57,6 @@ ...@@ -57,7 +57,6 @@
} }
.groups-header { .groups-header {
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
.nav-links { .nav-links {
width: 35%; width: 35%;
...@@ -68,3 +67,38 @@ ...@@ -68,3 +67,38 @@
} }
} }
} }
.groups-empty-state {
padding: 50px 100px;
overflow: hidden;
@media (max-width: $screen-md-min) {
padding: 50px 0;
}
svg {
float: right;
@media (max-width: $screen-md-min) {
float: none;
display: block;
width: 250px;
position: relative;
left: 50%;
margin-left: -125px;
}
}
.text-content {
float: left;
width: 460px;
margin-top: 120px;
@media (max-width: $screen-md-min) {
float: none;
margin-top: 60px;
width: auto;
text-align: center;
}
}
}
...@@ -33,6 +33,7 @@ ...@@ -33,6 +33,7 @@
// Issue title // Issue title
span a { span a {
color: $gl-text-color; color: $gl-text-color;
word-wrap: break-word;
} }
} }
} }
......
...@@ -743,6 +743,62 @@ pre.light-well { ...@@ -743,6 +743,62 @@ pre.light-well {
.dropdown-menu { .dropdown-menu {
width: 300px; width: 300px;
} }
&.from .compare-dropdown-toggle {
width: 237px;
}
&.to .compare-dropdown-toggle {
width: 254px;
}
.dropdown-toggle-text {
display: block;
height: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
}
.compare-ellipsis {
display: inline;
}
@media (max-width: $screen-xs-max) {
.compare-form-group {
.input-group {
width: 100%;
& > .compare-dropdown-toggle {
width: 100%;
}
}
.dropdown-menu {
width: 100%;
}
}
.compare-switch-container {
text-align: center;
padding: 0 0 $gl-padding;
.commits-compare-switch {
float: none;
}
}
.compare-ellipsis {
display: block;
text-align: center;
padding: 0 0 $gl-padding;
}
.commits-compare-btn {
width: 100%;
}
} }
.clearable-input { .clearable-input {
...@@ -779,4 +835,4 @@ pre.light-well { ...@@ -779,4 +835,4 @@ pre.light-well {
border-top-right-radius: 0; border-top-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
} }
} }
\ No newline at end of file
...@@ -10,7 +10,7 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -10,7 +10,7 @@ class Admin::GroupsController < Admin::ApplicationController
def show def show
@members = @group.members.order("access_level DESC").page(params[:members_page]) @members = @group.members.order("access_level DESC").page(params[:members_page])
@requesters = @group.requesters @requesters = AccessRequestsFinder.new(@group).execute(current_user)
@projects = @group.projects.page(params[:projects_page]) @projects = @group.projects.page(params[:projects_page])
end end
......
...@@ -22,7 +22,7 @@ class Admin::ProjectsController < Admin::ApplicationController ...@@ -22,7 +22,7 @@ class Admin::ProjectsController < Admin::ApplicationController
end end
@project_members = @project.members.page(params[:project_members_page]) @project_members = @project.members.page(params[:project_members_page])
@requesters = @project.requesters @requesters = AccessRequestsFinder.new(@project).execute(current_user)
end end
def transfer def transfer
......
...@@ -14,6 +14,7 @@ module Ci ...@@ -14,6 +14,7 @@ module Ci
@config_processor = Ci::GitlabCiYamlProcessor.new(@content) @config_processor = Ci::GitlabCiYamlProcessor.new(@content)
@stages = @config_processor.stages @stages = @config_processor.stages
@builds = @config_processor.builds @builds = @config_processor.builds
@jobs = @config_processor.jobs
end end
rescue rescue
@error = 'Undefined error' @error = 'Undefined error'
......
...@@ -15,7 +15,7 @@ class Groups::GroupMembersController < Groups::ApplicationController ...@@ -15,7 +15,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end end
@members = @members.order('access_level DESC').page(params[:page]).per(50) @members = @members.order('access_level DESC').page(params[:page]).per(50)
@requesters = @group.requesters if can?(current_user, :admin_group, @group) @requesters = AccessRequestsFinder.new(@group).execute(current_user)
@group_member = @group.group_members.new @group_member = @group.group_members.new
end end
......
class Import::GitlabProjectsController < Import::BaseController class Import::GitlabProjectsController < Import::BaseController
before_action :verify_gitlab_project_import_enabled before_action :verify_gitlab_project_import_enabled
before_action :authenticate_admin!
def new def new
@namespace_id = project_params[:namespace_id] @namespace_id = project_params[:namespace_id]
...@@ -48,8 +47,4 @@ class Import::GitlabProjectsController < Import::BaseController ...@@ -48,8 +47,4 @@ class Import::GitlabProjectsController < Import::BaseController
:path, :namespace_id, :file :path, :namespace_id, :file
) )
end end
def authenticate_admin!
render_404 unless current_user.is_admin?
end
end end
...@@ -33,7 +33,7 @@ module Projects ...@@ -33,7 +33,7 @@ module Projects
def issue def issue
@issue ||= @issue ||=
IssuesFinder.new(current_user, project_id: project.id, state: 'all') IssuesFinder.new(current_user, project_id: project.id)
.execute .execute
.where(iid: params[:id]) .where(iid: params[:id])
.first! .first!
......
...@@ -18,6 +18,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -18,6 +18,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :define_commit_vars, only: [:diffs] before_action :define_commit_vars, only: [:diffs]
before_action :define_diff_comment_vars, only: [:diffs] before_action :define_diff_comment_vars, only: [:diffs]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines] before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines]
before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
# Allow read any merge_request # Allow read any merge_request
before_action :authorize_read_merge_request! before_action :authorize_read_merge_request!
...@@ -275,7 +276,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -275,7 +276,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
def remove_wip def remove_wip
MergeRequests::UpdateService.new(project, current_user, title: @merge_request.wipless_title).execute(@merge_request) MergeRequests::UpdateService.new(project, current_user, wip_event: 'unwip').execute(@merge_request)
redirect_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), redirect_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request),
notice: "The merge request can now be merged." notice: "The merge request can now be merged."
...@@ -308,8 +309,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -308,8 +309,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
return return
end end
TodoService.new.merge_merge_request(merge_request, current_user)
@merge_request.update(merge_error: nil) @merge_request.update(merge_error: nil)
if params[:merge_when_build_succeeds].present? if params[:merge_when_build_succeeds].present?
...@@ -418,10 +417,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -418,10 +417,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
def validates_merge_request def validates_merge_request
# If source project was removed and merge request for some reason
# wasn't close (Ex. mr from fork to origin)
return invalid_mr if !@merge_request.source_project && @merge_request.open?
# Show git not found page # Show git not found page
# if there is no saved commits between source & target branch # if there is no saved commits between source & target branch
if @merge_request.commits.blank? if @merge_request.commits.blank?
...@@ -496,7 +491,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -496,7 +491,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
def invalid_mr def invalid_mr
# Render special view for MR with removed source or target branch # Render special view for MR with removed target branch
render 'invalid' render 'invalid'
end end
...@@ -538,4 +533,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -538,4 +533,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@diff_notes_disabled = !@merge_request_diff.latest? @diff_notes_disabled = !@merge_request_diff.latest?
@diffs = @merge_request_diff.diffs(diff_options) @diffs = @merge_request_diff.diffs(diff_options)
end end
def close_merge_request_without_source_project
if !@merge_request.source_project && @merge_request.open?
@merge_request.close
end
end
end end
...@@ -29,7 +29,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController ...@@ -29,7 +29,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group_members = @group_members.order('access_level DESC') @group_members = @group_members.order('access_level DESC')
end end
@requesters = @project.requesters if can?(current_user, :admin_project, @project) @requesters = AccessRequestsFinder.new(@project).execute(current_user)
@project_member = @project.project_members.new @project_member = @project.project_members.new
@project_group_links = @project.project_group_links @project_group_links = @project.project_group_links
......
...@@ -137,10 +137,10 @@ class ProjectsController < Projects::ApplicationController ...@@ -137,10 +137,10 @@ class ProjectsController < Projects::ApplicationController
noteable = noteable =
case params[:type] case params[:type]
when 'Issue' when 'Issue'
IssuesFinder.new(current_user, project_id: @project.id, state: 'all'). IssuesFinder.new(current_user, project_id: @project.id).
execute.find_by(iid: params[:type_id]) execute.find_by(iid: params[:type_id])
when 'MergeRequest' when 'MergeRequest'
MergeRequestsFinder.new(current_user, project_id: @project.id, state: 'all'). MergeRequestsFinder.new(current_user, project_id: @project.id).
execute.find_by(iid: params[:type_id]) execute.find_by(iid: params[:type_id])
when 'Commit' when 'Commit'
@project.commit(params[:type_id]) @project.commit(params[:type_id])
......
class AccessRequestsFinder
attr_accessor :source
# Arguments:
# source - a Group or Project
def initialize(source)
@source = source
end
def execute(*args)
execute!(*args)
rescue Gitlab::Access::AccessDeniedError
[]
end
def execute!(current_user)
raise Gitlab::Access::AccessDeniedError unless can_see_access_requests?(current_user)
source.requesters
end
private
def can_see_access_requests?(current_user)
source && Ability.allowed?(current_user, :"admin_#{source.class.to_s.underscore}", source)
end
end
...@@ -183,17 +183,12 @@ class IssuableFinder ...@@ -183,17 +183,12 @@ class IssuableFinder
end end
def by_state(items) def by_state(items)
case params[:state] params[:state] ||= 'all'
when 'closed'
items.closed if items.respond_to?(params[:state])
when 'merged' items.public_send(params[:state])
items.respond_to?(:merged) ? items.merged : items.closed
when 'all'
items
when 'opened'
items.opened
else else
raise 'You must specify default state' items
end end
end end
......
...@@ -280,32 +280,6 @@ module ApplicationHelper ...@@ -280,32 +280,6 @@ module ApplicationHelper
end end
end end
def state_filters_text_for(entity, project)
titles = {
opened: "Open"
}
entity_title = titles[entity] || entity.to_s.humanize
count =
if project.nil?
nil
elsif current_controller?(:issues)
project.issues.visible_to_user(current_user).send(entity).count
elsif current_controller?(:merge_requests)
project.merge_requests.send(entity).count
end
html = content_tag :span, entity_title
if count.present?
html += " "
html += content_tag :span, number_with_delimiter(count), class: 'badge'
end
html.html_safe
end
def truncate_first_line(message, length = 50) def truncate_first_line(message, length = 50)
truncate(message.each_line.first.chomp, length: length) if message truncate(message.each_line.first.chomp, length: length) if message
end end
......
...@@ -94,6 +94,24 @@ module IssuablesHelper ...@@ -94,6 +94,24 @@ module IssuablesHelper
label_names.join(', ') label_names.join(', ')
end end
def issuables_state_counter_text(issuable_type, state)
titles = {
opened: "Open"
}
state_title = titles[state] || state.to_s.humanize
count =
Rails.cache.fetch(issuables_state_counter_cache_key(issuable_type, state), expires_in: 2.minutes) do
issuables_count_for_state(issuable_type, state)
end
html = content_tag(:span, state_title)
html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge')
html.html_safe
end
private private
def sidebar_gutter_collapsed? def sidebar_gutter_collapsed?
...@@ -111,4 +129,22 @@ module IssuablesHelper ...@@ -111,4 +129,22 @@ module IssuablesHelper
issuable.open? ? :opened : :closed issuable.open? ? :opened : :closed
end end
end end
def issuables_count_for_state(issuable_type, state)
issuables_finder = public_send("#{issuable_type}_finder")
issuables_finder.params[:state] = state
issuables_finder.execute.page(1).total_count
end
IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page]
private_constant :IRRELEVANT_PARAMS_FOR_CACHE_KEY
def issuables_state_counter_cache_key(issuable_type, state)
opts = params.with_indifferent_access
opts[:state] = state
opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY)
hexdigest(['issuables_count', issuable_type, opts.sort].flatten.join('-'))
end
end end
...@@ -373,7 +373,7 @@ module Ci ...@@ -373,7 +373,7 @@ module Ci
end end
def artifacts? def artifacts?
!artifacts_expired? && self[:artifacts_file].present? !artifacts_expired? && artifacts_file.exists?
end end
def artifacts_metadata? def artifacts_metadata?
......
...@@ -8,9 +8,6 @@ module AccessRequestable ...@@ -8,9 +8,6 @@ module AccessRequestable
extend ActiveSupport::Concern extend ActiveSupport::Concern
def request_access(user) def request_access(user)
members.create( Members::RequestAccessService.new(self, user).execute
access_level: Gitlab::Access::DEVELOPER,
user: user,
requested_at: Time.now.utc)
end end
end end
...@@ -102,40 +102,44 @@ class Group < Namespace ...@@ -102,40 +102,44 @@ class Group < Namespace
self[:lfs_enabled] self[:lfs_enabled]
end end
def add_users(user_ids, access_level, current_user: nil, expires_at: nil) def add_users(users, access_level, current_user: nil, expires_at: nil)
user_ids.each do |user_id| GroupMember.add_users_to_group(
Member.add_user( self,
self.group_members, users,
user_id, access_level,
access_level, current_user: current_user,
current_user: current_user, expires_at: expires_at
expires_at: expires_at )
)
end
end end
def add_user(user, access_level, current_user: nil, expires_at: nil) def add_user(user, access_level, current_user: nil, expires_at: nil)
add_users([user], access_level, current_user: current_user, expires_at: expires_at) GroupMember.add_user(
self,
user,
access_level,
current_user: current_user,
expires_at: expires_at
)
end end
def add_guest(user, current_user = nil) def add_guest(user, current_user = nil)
add_user(user, Gitlab::Access::GUEST, current_user: current_user) add_user(user, :guest, current_user: current_user)
end end
def add_reporter(user, current_user = nil) def add_reporter(user, current_user = nil)
add_user(user, Gitlab::Access::REPORTER, current_user: current_user) add_user(user, :reporter, current_user: current_user)
end end
def add_developer(user, current_user = nil) def add_developer(user, current_user = nil)
add_user(user, Gitlab::Access::DEVELOPER, current_user: current_user) add_user(user, :developer, current_user: current_user)
end end
def add_master(user, current_user = nil) def add_master(user, current_user = nil)
add_user(user, Gitlab::Access::MASTER, current_user: current_user) add_user(user, :master, current_user: current_user)
end end
def add_owner(user, current_user = nil) def add_owner(user, current_user = nil)
add_user(user, Gitlab::Access::OWNER, current_user: current_user) add_user(user, :owner, current_user: current_user)
end end
def has_owner?(user) def has_owner?(user)
......
...@@ -80,49 +80,70 @@ class Member < ActiveRecord::Base ...@@ -80,49 +80,70 @@ class Member < ActiveRecord::Base
find_by(invite_token: invite_token) find_by(invite_token: invite_token)
end end
# This method is used to find users that have been entered into the "Add members" field. def add_user(source, user, access_level, current_user: nil, expires_at: nil)
# These can be the User objects directly, their IDs, their emails, or new emails to be invited. user = retrieve_user(user)
def user_for_id(user_id) access_level = retrieve_access_level(access_level)
return user_id if user_id.is_a?(User)
user = User.find_by(id: user_id)
user ||= User.find_by(email: user_id)
user ||= user_id
user
end
def add_user(members, user_id, access_level, current_user: nil, expires_at: nil)
user = user_for_id(user_id)
# `user` can be either a User object or an email to be invited # `user` can be either a User object or an email to be invited
if user.is_a?(User) member =
member = members.find_or_initialize_by(user_id: user.id) if user.is_a?(User)
source.members.find_by(user_id: user.id) ||
source.requesters.find_by(user_id: user.id) ||
source.members.build(user_id: user.id)
else
source.members.build(invite_email: user)
end
return member unless can_update_member?(current_user, member)
member.attributes = {
created_by: member.created_by || current_user,
access_level: access_level,
expires_at: expires_at
}
if member.request?
::Members::ApproveAccessRequestService.new(source, current_user, id: member.id).execute
else else
member = members.build member.save
member.invite_email = user
end end
if can_update_member?(current_user, member) || project_creator?(member, access_level) member
member.created_by ||= current_user end
member.access_level = access_level
member.expires_at = expires_at
member.save def access_levels
end Gitlab::Access.sym_options
end end
private private
# This method is used to find users that have been entered into the "Add members" field.
# These can be the User objects directly, their IDs, their emails, or new emails to be invited.
def retrieve_user(user)
return user if user.is_a?(User)
User.find_by(id: user) || User.find_by(email: user) || user
end
def retrieve_access_level(access_level)
access_levels.fetch(access_level) { access_level.to_i }
end
def can_update_member?(current_user, member) def can_update_member?(current_user, member)
# There is no current user for bulk actions, in which case anything is allowed # There is no current user for bulk actions, in which case anything is allowed
!current_user || !current_user || current_user.can?(:"update_#{member.type.underscore}", member)
current_user.can?(:update_group_member, member) ||
current_user.can?(:update_project_member, member)
end end
def project_creator?(member, access_level) def add_users_to_source(source, users, access_level, current_user: nil, expires_at: nil)
member.new_record? && member.owner? && users.each do |user|
access_level.to_i == ProjectMember::MASTER add_user(
source,
user,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
end end
end end
......
...@@ -12,6 +12,22 @@ class GroupMember < Member ...@@ -12,6 +12,22 @@ class GroupMember < Member
Gitlab::Access.options_with_owner Gitlab::Access.options_with_owner
end end
def self.access_levels
Gitlab::Access.sym_options_with_owner
end
def self.add_users_to_group(group, users, access_level, current_user: nil, expires_at: nil)
self.transaction do
add_users_to_source(
group,
users,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
end
def group def group
source source
end end
......
...@@ -34,36 +34,20 @@ class ProjectMember < Member ...@@ -34,36 +34,20 @@ class ProjectMember < Member
# :master # :master
# ) # )
# #
def add_users_to_projects(project_ids, user_ids, access, current_user: nil, expires_at: nil) def add_users_to_projects(project_ids, users, access_level, current_user: nil, expires_at: nil)
access_level = if roles_hash.has_key?(access) self.transaction do
roles_hash[access]
elsif roles_hash.values.include?(access.to_i)
access
else
raise "Non valid access"
end
users = user_ids.map { |user_id| Member.user_for_id(user_id) }
ProjectMember.transaction do
project_ids.each do |project_id| project_ids.each do |project_id|
project = Project.find(project_id) project = Project.find(project_id)
users.each do |user| add_users_to_source(
Member.add_user( project,
project.project_members, users,
user, access_level,
access_level, current_user: current_user,
current_user: current_user, expires_at: expires_at
expires_at: expires_at )
)
end
end end
end end
true
rescue
false
end end
def truncate_teams(project_ids) def truncate_teams(project_ids)
...@@ -84,13 +68,15 @@ class ProjectMember < Member ...@@ -84,13 +68,15 @@ class ProjectMember < Member
truncate_teams [project.id] truncate_teams [project.id]
end end
def roles_hash
Gitlab::Access.sym_options
end
def access_level_roles def access_level_roles
Gitlab::Access.options Gitlab::Access.options
end end
private
def can_update_member?(current_user, member)
super || (member.owner? && member.new_record?)
end
end end
def access_field def access_field
......
...@@ -155,6 +155,20 @@ class MergeRequest < ActiveRecord::Base ...@@ -155,6 +155,20 @@ class MergeRequest < ActiveRecord::Base
where("merge_requests.id IN (#{union.to_sql})") where("merge_requests.id IN (#{union.to_sql})")
end end
WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
def self.work_in_progress?(title)
!!(title =~ WIP_REGEX)
end
def self.wipless_title(title)
title.sub(WIP_REGEX, "")
end
def self.wip_title(title)
work_in_progress?(title) ? title : "WIP: #{title}"
end
def to_reference(from_project = nil) def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}" reference = "#{self.class.reference_prefix}#{iid}"
...@@ -389,14 +403,16 @@ class MergeRequest < ActiveRecord::Base ...@@ -389,14 +403,16 @@ class MergeRequest < ActiveRecord::Base
@closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last @closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
end end
WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
def work_in_progress? def work_in_progress?
!!(title =~ WIP_REGEX) self.class.work_in_progress?(title)
end end
def wipless_title def wipless_title
self.title.sub(WIP_REGEX, "") self.class.wipless_title(self.title)
end
def wip_title
self.class.wip_title(self.title)
end end
def mergeable?(skip_ci_check: false) def mergeable?(skip_ci_check: false)
......
...@@ -158,7 +158,7 @@ class Milestone < ActiveRecord::Base ...@@ -158,7 +158,7 @@ class Milestone < ActiveRecord::Base
end end
def title=(value) def title=(value)
write_attribute(:title, Sanitize.clean(value.to_s)) if value.present? write_attribute(:title, sanitize_title(value)) if value.present?
end end
# Sorts the issues for the given IDs. # Sorts the issues for the given IDs.
...@@ -204,4 +204,8 @@ class Milestone < ActiveRecord::Base ...@@ -204,4 +204,8 @@ class Milestone < ActiveRecord::Base
iid iid
end end
end end
def sanitize_title(value)
CGI.unescape_html(Sanitize.clean(value.to_s))
end
end end
...@@ -146,6 +146,7 @@ class Project < ActiveRecord::Base ...@@ -146,6 +146,7 @@ class Project < ActiveRecord::Base
delegate :name, to: :owner, allow_nil: true, prefix: true delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true delegate :members, to: :team, prefix: true
delegate :add_user, to: :team
# Validations # Validations
validates :creator, presence: true, on: :create validates :creator, presence: true, on: :create
...@@ -1016,10 +1017,6 @@ class Project < ActiveRecord::Base ...@@ -1016,10 +1017,6 @@ class Project < ActiveRecord::Base
project_members.find_by(user_id: user) project_members.find_by(user_id: user)
end end
def add_user(user, access_level, current_user: nil, expires_at: nil)
team.add_user(user, access_level, current_user: current_user, expires_at: expires_at)
end
def default_branch def default_branch
@default_branch ||= repository.root_ref if repository.exists? @default_branch ||= repository.root_ref if repository.exists?
end end
......
...@@ -33,18 +33,24 @@ class ProjectTeam ...@@ -33,18 +33,24 @@ class ProjectTeam
member member
end end
def add_users(users, access, current_user: nil, expires_at: nil) def add_users(users, access_level, current_user: nil, expires_at: nil)
ProjectMember.add_users_to_projects( ProjectMember.add_users_to_projects(
[project.id], [project.id],
users, users,
access, access_level,
current_user: current_user, current_user: current_user,
expires_at: expires_at expires_at: expires_at
) )
end end
def add_user(user, access, current_user: nil, expires_at: nil) def add_user(user, access_level, current_user: nil, expires_at: nil)
add_users([user], access, current_user: current_user, expires_at: expires_at) ProjectMember.add_user(
project,
user,
access_level,
current_user: current_user,
expires_at: expires_at
)
end end
# Remove all users from project team # Remove all users from project team
......
module Members
class RequestAccessService < BaseService
attr_accessor :source
def initialize(source, current_user)
@source = source
@current_user = current_user
end
def execute
raise Gitlab::Access::AccessDeniedError unless can_request_access?(source)
source.members.create(
access_level: Gitlab::Access::DEVELOPER,
user: current_user,
requested_at: Time.now.utc)
end
private
def can_request_access?(source)
source && can?(current_user, :request_access, source)
end
end
end
...@@ -5,16 +5,17 @@ module MergeRequests ...@@ -5,16 +5,17 @@ module MergeRequests
end end
def create_title_change_note(issuable, old_title) def create_title_change_note(issuable, old_title)
removed_wip = old_title =~ MergeRequest::WIP_REGEX && !issuable.work_in_progress? removed_wip = MergeRequest.work_in_progress?(old_title) && !issuable.work_in_progress?
added_wip = old_title !~ MergeRequest::WIP_REGEX && issuable.work_in_progress? added_wip = !MergeRequest.work_in_progress?(old_title) && issuable.work_in_progress?
changed_title = MergeRequest.wipless_title(old_title) != issuable.wipless_title
if removed_wip if removed_wip
SystemNoteService.remove_merge_request_wip(issuable, issuable.project, current_user) SystemNoteService.remove_merge_request_wip(issuable, issuable.project, current_user)
elsif added_wip elsif added_wip
SystemNoteService.add_merge_request_wip(issuable, issuable.project, current_user) SystemNoteService.add_merge_request_wip(issuable, issuable.project, current_user)
else
super
end end
super if changed_title
end end
def hook_data(merge_request, action, oldrev = nil) def hook_data(merge_request, action, oldrev = nil)
......
...@@ -7,6 +7,7 @@ module MergeRequests ...@@ -7,6 +7,7 @@ module MergeRequests
class PostMergeService < MergeRequests::BaseService class PostMergeService < MergeRequests::BaseService
def execute(merge_request) def execute(merge_request)
close_issues(merge_request) close_issues(merge_request)
todo_service.merge_merge_request(merge_request, current_user)
merge_request.mark_as_merged merge_request.mark_as_merged
create_merge_event(merge_request, current_user) create_merge_event(merge_request, current_user)
create_note(merge_request) create_note(merge_request)
......
...@@ -16,7 +16,7 @@ module MergeRequests ...@@ -16,7 +16,7 @@ module MergeRequests
end end
merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch) merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
handle_wip_event(merge_request)
update(merge_request) update(merge_request)
end end
...@@ -81,5 +81,18 @@ module MergeRequests ...@@ -81,5 +81,18 @@ module MergeRequests
def after_update(issuable) def after_update(issuable)
issuable.cache_merge_request_closes_issues!(current_user) issuable.cache_merge_request_closes_issues!(current_user)
end end
private
def handle_wip_event(merge_request)
if wip_event = params.delete(:wip_event)
# We update the title that is provided in the params or we use the mr title
title = params[:title] || merge_request.title
params[:title] = case wip_event
when 'wip' then MergeRequest.wip_title(title)
when 'unwip' then MergeRequest.wipless_title(title)
end
end
end
end end
end end
...@@ -134,7 +134,8 @@ class NotificationService ...@@ -134,7 +134,8 @@ class NotificationService
merge_request, merge_request,
merge_request.target_project, merge_request.target_project,
current_user, current_user,
:merged_merge_request_email :merged_merge_request_email,
skip_current_user: !merge_request.merge_when_build_succeeds?
) )
end end
...@@ -514,9 +515,16 @@ class NotificationService ...@@ -514,9 +515,16 @@ class NotificationService
end end
end end
def close_resource_email(target, project, current_user, method) def close_resource_email(target, project, current_user, method, skip_current_user: true)
action = method == :merged_merge_request_email ? "merge" : "close" action = method == :merged_merge_request_email ? "merge" : "close"
recipients = build_recipients(target, project, current_user, action: action)
recipients = build_recipients(
target,
project,
current_user,
action: action,
skip_current_user: skip_current_user
)
recipients.each do |recipient| recipients.each do |recipient|
mailer.send(method, recipient.id, target.id, current_user.id).deliver_later mailer.send(method, recipient.id, target.id, current_user.id).deliver_later
...@@ -557,7 +565,7 @@ class NotificationService ...@@ -557,7 +565,7 @@ class NotificationService
end end
end end
def build_recipients(target, project, current_user, action: nil, previous_assignee: nil) def build_recipients(target, project, current_user, action: nil, previous_assignee: nil, skip_current_user: true)
custom_action = build_custom_key(action, target) custom_action = build_custom_key(action, target)
recipients = target.participants(current_user) recipients = target.participants(current_user)
...@@ -586,7 +594,8 @@ class NotificationService ...@@ -586,7 +594,8 @@ class NotificationService
recipients = reject_unsubscribed_users(recipients, target) recipients = reject_unsubscribed_users(recipients, target)
recipients = reject_users_without_access(recipients, target) recipients = reject_users_without_access(recipients, target)
recipients.delete(current_user) recipients.delete(current_user) if skip_current_user
recipients.uniq recipients.uniq
end end
......
...@@ -214,6 +214,18 @@ module SlashCommands ...@@ -214,6 +214,18 @@ module SlashCommands
@updates[:due_date] = nil @updates[:due_date] = nil
end end
desc do
"Toggle the Work In Progress status"
end
condition do
issuable.persisted? &&
issuable.respond_to?(:work_in_progress?) &&
current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
end
command :wip do
@updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip'
end
# This is a dummy command, so that it appears in the autocomplete commands # This is a dummy command, so that it appears in the autocomplete commands
desc 'CC' desc 'CC'
params '@user' params '@user'
......
...@@ -24,6 +24,7 @@ module SystemNoteService ...@@ -24,6 +24,7 @@ module SystemNoteService
body = "Added #{commits_text}:\n\n" body = "Added #{commits_text}:\n\n"
body << existing_commit_summary(noteable, existing_commits, oldrev) body << existing_commit_summary(noteable, existing_commits, oldrev)
body << new_commit_summary(new_commits).join("\n") body << new_commit_summary(new_commits).join("\n")
body << "\n\n[Compare with previous version](#{diff_comparison_url(noteable, project, oldrev)})"
create_note(noteable: noteable, project: project, author: author, note: body) create_note(noteable: noteable, project: project, author: author, note: body)
end end
...@@ -254,8 +255,7 @@ module SystemNoteService ...@@ -254,8 +255,7 @@ module SystemNoteService
# #
# "Started branch `201-issue-branch-button`" # "Started branch `201-issue-branch-button`"
def new_issue_branch(issue, project, author, branch) def new_issue_branch(issue, project, author, branch)
h = Gitlab::Routing.url_helpers link = url_helpers.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
body = "Started branch [`#{branch}`](#{link})" body = "Started branch [`#{branch}`](#{link})"
create_note(noteable: issue, project: project, author: author, note: body) create_note(noteable: issue, project: project, author: author, note: body)
...@@ -466,4 +466,20 @@ module SystemNoteService ...@@ -466,4 +466,20 @@ module SystemNoteService
def escape_html(text) def escape_html(text)
Rack::Utils.escape_html(text) Rack::Utils.escape_html(text)
end end
def url_helpers
@url_helpers ||= Gitlab::Routing.url_helpers
end
def diff_comparison_url(merge_request, project, oldrev)
diff_id = merge_request.merge_request_diff.id
url_helpers.diffs_namespace_project_merge_request_url(
project.namespace,
project,
merge_request.iid,
diff_id: diff_id,
start_sha: oldrev
)
end
end end
.scrolling-tabs-container.sub-nav-scroll = content_for :sub_nav do
= render 'shared/nav_scroll' .scrolling-tabs-container.sub-nav-scroll
.nav-links.sub-nav.scrolling-tabs = render 'shared/nav_scroll'
%ul{ class: (container_class) } .nav-links.sub-nav.scrolling-tabs
= nav_link(controller: :system_info) do %ul{ class: (container_class) }
= link_to admin_system_info_path, title: 'System Info' do = nav_link(controller: :system_info) do
%span = link_to admin_system_info_path, title: 'System Info' do
System Info %span
= nav_link(controller: :background_jobs) do System Info
= link_to admin_background_jobs_path, title: 'Background Jobs' do = nav_link(controller: :background_jobs) do
%span = link_to admin_background_jobs_path, title: 'Background Jobs' do
Background Jobs %span
= nav_link(controller: :logs) do Background Jobs
= link_to admin_logs_path, title: 'Logs' do = nav_link(controller: :logs) do
%span = link_to admin_logs_path, title: 'Logs' do
Logs %span
= nav_link(controller: :health_check) do Logs
= link_to admin_health_check_path, title: 'Health Check' do = nav_link(controller: :health_check) do
%span = link_to admin_health_check_path, title: 'Health Check' do
Health Check %span
= nav_link(controller: :requests_profiles) do Health Check
= link_to admin_requests_profiles_path, title: 'Requests Profiles' do = nav_link(controller: :requests_profiles) do
%span = link_to admin_requests_profiles_path, title: 'Requests Profiles' do
Requests Profiles %span
Requests Profiles
.scrolling-tabs-container.sub-nav-scroll = content_for :sub_nav do
= render 'shared/nav_scroll' .scrolling-tabs-container.sub-nav-scroll
.nav-links.sub-nav.scrolling-tabs = render 'shared/nav_scroll'
%ul{ class: (container_class) } .nav-links.sub-nav.scrolling-tabs
= nav_link(controller: :dashboard, html_options: {class: 'home'}) do %ul{ class: (container_class) }
= link_to admin_root_path, title: 'Overview' do = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
%span = link_to admin_root_path, title: 'Overview' do
Overview %span
= nav_link(controller: [:admin, :projects]) do Overview
= link_to admin_namespaces_projects_path, title: 'Projects' do = nav_link(controller: [:admin, :projects]) do
%span = link_to admin_namespaces_projects_path, title: 'Projects' do
Projects %span
= nav_link(controller: :users) do Projects
= link_to admin_users_path, title: 'Users' do = nav_link(controller: :users) do
%span = link_to admin_users_path, title: 'Users' do
Users %span
= nav_link(controller: :groups) do Users
= link_to admin_groups_path, title: 'Groups' do = nav_link(controller: :groups) do
%span = link_to admin_groups_path, title: 'Groups' do
Groups %span
= nav_link path: 'builds#index' do Groups
= link_to admin_builds_path, title: 'Builds' do = nav_link path: 'builds#index' do
%span = link_to admin_builds_path, title: 'Builds' do
Builds %span
= nav_link path: ['runners#index', 'runners#show'] do Builds
= link_to admin_runners_path, title: 'Runners' do = nav_link path: ['runners#index', 'runners#show'] do
%span = link_to admin_runners_path, title: 'Runners' do
Runners %span
Runners
...@@ -21,13 +21,16 @@ ...@@ -21,13 +21,16 @@
%br %br
%b Tag list: %b Tag list:
= build[:tags] = build[:tag_list].to_a.join(", ")
%br %br
%b Refs only: %b Refs only:
= build[:only] && build[:only].join(", ") = @jobs[build[:name].to_sym][:only].to_a.join(", ")
%br %br
%b Refs except: %b Refs except:
= build[:except] && build[:except].join(", ") = @jobs[build[:name].to_sym][:except].to_a.join(", ")
%br
%b Environment:
= build[:environment]
%br %br
%b When: %b When:
= build[:when] = build[:when]
......
.groups-empty-state
= custom_icon("icon_empty_groups")
.text-content
%h4 A group is a collection of several projects.
%p If you organize your projects under a group, it works like a folder.
%p You can manage your group member’s permissions and access to each project in the group.
...@@ -2,9 +2,12 @@ ...@@ -2,9 +2,12 @@
- header_title "Groups", dashboard_groups_path - header_title "Groups", dashboard_groups_path
= render 'dashboard/groups_head' = render 'dashboard/groups_head'
%ul.content-list - if @group_members.empty?
- @group_members.each do |group_member| = render 'empty_state'
- group = group_member.group - else
= render 'shared/groups/group', group: group, group_member: group_member %ul.content-list
- @group_members.each do |group_member|
- group = group_member.group
= render 'shared/groups/group', group: group, group_member: group_member
= paginate @group_members, theme: 'gitlab' = paginate @group_members, theme: 'gitlab'
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
- else - else
Any Any
%b.caret %b.caret
%ul.dropdown-menu %ul.dropdown-menu.dropdown-menu-align-right
%li %li
= link_to filter_projects_path(visibility_level: nil) do = link_to filter_projects_path(visibility_level: nil) do
Any Any
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
- else - else
Any Any
%b.caret %b.caret
%ul.dropdown-menu %ul.dropdown-menu.dropdown-menu-align-right
%li %li
= link_to filter_projects_path(tag: nil) do = link_to filter_projects_path(tag: nil) do
Any Any
......
.flash-container.flash-container-page .flash-container.flash-container-page
- if alert - if alert
.flash-alert .flash-alert
= alert %div{ class: (container_class) }
%span= alert
- elsif notice - elsif notice
.flash-notice .flash-notice
= notice %div{ class: (container_class) }
%span= notice
.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } .page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" }
.sidebar-wrapper.nicescroll .sidebar-wrapper.nicescroll
.sidebar-action-buttons .sidebar-action-buttons
= link_to '#', class: 'nav-header-btn toggle-nav-collapse', title: "Open/Close" do .nav-header-btn.toggle-nav-collapse{ title: "Open/Close" }
%span.sr-only Toggle navigation %span.sr-only Toggle navigation
= icon('bars') = icon('bars')
= link_to '#', class: "nav-header-btn pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: {placement: 'right', container: 'body'} do
%div{ class: "nav-header-btn pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: { placement: 'right', container: 'body' } }
%span.sr-only Toggle navigation pinning %span.sr-only Toggle navigation pinning
= icon('fw thumb-tack') = icon('fw thumb-tack')
...@@ -20,6 +21,7 @@ ...@@ -20,6 +21,7 @@
.container-fluid .container-fluid
= render "layouts/nav/#{nav}" = render "layouts/nav/#{nav}"
.content-wrapper{ class: "#{layout_nav_class}" } .content-wrapper{ class: "#{layout_nav_class}" }
= yield :sub_nav
= render "layouts/broadcast" = render "layouts/broadcast"
= render "layouts/flash" = render "layouts/flash"
= yield :flash_message = yield :flash_message
......
.nav-block.activity-filter-block - @no_container = true
- if current_user
.controls
= link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do
%i.fa.fa-rss
= render 'shared/event_filter' %div{ class: container_class }
.nav-block.activity-filter-block
- if current_user
.controls
= link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do
= icon('rss')
.content_list.project-activity{:"data-href" => activity_project_path(@project)} = render 'shared/event_filter'
= spinner
.content_list.project-activity{:"data-href" => activity_project_path(@project)}
= spinner
:javascript :javascript
var activity = new Activities(); var activity = new Activities();
......
- if event = last_push_event - if event = last_push_event
- if show_last_push_widget?(event) - if show_last_push_widget?(event)
.row-content-block.top-block.clear-block.hidden-xs .row-content-block.top-block.hidden-xs.white
%div{ class: container_class } %div{ class: container_class }
.event-last-push .event-last-push
.event-last-push-text .event-last-push-text
......
...@@ -21,6 +21,13 @@ ...@@ -21,6 +21,13 @@
= dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } ) = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.hidden .gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.hidden
= dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } ) = dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
= button_tag class: 'soft-wrap-toggle btn', type: 'button' do
%span.no-wrap
= custom_icon('icon_no_wrap')
No wrap
%span.soft-wrap
= custom_icon('icon_soft_wrap')
Soft wrap
.encoding-selector .encoding-selector
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
%a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" } %a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" }
= icon('angle-double-right') = icon('angle-double-right')
- if @build.coverage - if @build.coverage
.block.block-first .block.coverage
.title .title
Test coverage Test coverage
%p.build-detail-row %p.build-detail-row
...@@ -95,7 +95,7 @@ ...@@ -95,7 +95,7 @@
- @build.trigger_request.variables.each do |key, value| - @build.trigger_request.variables.each do |key, value|
.hide.js-build .hide.js-build
.js-build-variable= key .js-build-variable= key
.js-build-value= value .js-build-value= value
.block .block
...@@ -128,7 +128,7 @@ ...@@ -128,7 +128,7 @@
- builds.select{|build| build.status == build_status}.each do |build| - builds.select{|build| build.status == build_status}.each do |build|
.build-job{class: ('active' if build == @build), data: {stage: build.stage}} .build-job{class: ('active' if build == @build), data: {stage: build.stage}}
= link_to namespace_project_build_path(@project.namespace, @project, build) do = link_to namespace_project_build_path(@project.namespace, @project, build) do
= icon('check') = icon('right-arrow')
= ci_icon_for_status(build.status) = ci_icon_for_status(build.status)
%span %span
- if build.name - if build.name
......
.scrolling-tabs-container.sub-nav-scroll = content_for :sub_nav do
= render 'shared/nav_scroll' .scrolling-tabs-container.sub-nav-scroll
.nav-links.sub-nav.scrolling-tabs = render 'shared/nav_scroll'
%ul{ class: (container_class) } .nav-links.sub-nav.scrolling-tabs
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do %ul{ class: (container_class) }
= link_to project_files_path(@project) do = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
Files = link_to project_files_path(@project) do
Files
= nav_link(controller: [:commit, :commits]) do = nav_link(controller: [:commit, :commits]) do
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
Commits Commits
= nav_link(controller: %w(network)) do = nav_link(controller: %w(network)) do
= link_to namespace_project_network_path(@project.namespace, @project, current_ref) do = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do
Network Network
= nav_link(controller: :compare) do = nav_link(controller: :compare) do
= link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do
Compare Compare
= nav_link(html_options: {class: branches_tab_class}) do = nav_link(html_options: {class: branches_tab_class}) do
= link_to namespace_project_branches_path(@project.namespace, @project) do = link_to namespace_project_branches_path(@project.namespace, @project) do
Branches Branches
= nav_link(controller: [:tags, :releases]) do = nav_link(controller: [:tags, :releases]) do
= link_to namespace_project_tags_path(@project.namespace, @project) do = link_to namespace_project_tags_path(@project.namespace, @project) do
Tags Tags
...@@ -5,7 +5,8 @@ ...@@ -5,7 +5,8 @@
- if current_user - if current_user
= auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits")
= render "head" = content_for :sub_nav do
= render "head"
%div{ class: container_class } %div{ class: container_class }
.row-content-block.second-block.content-component-block .row-content-block.second-block.content-component-block
......
= form_tag namespace_project_compare_index_path(@project.namespace, @project), method: :post, class: 'form-inline js-requires-input' do = form_tag namespace_project_compare_index_path(@project.namespace, @project), method: :post, class: 'form-inline js-requires-input' do
.clearfix .clearfix
- if params[:to] && params[:from] - if params[:to] && params[:from]
= link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'} .compare-switch-container
.form-group.dropdown.compare-form-group.js-compare-from-dropdown = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'}
.form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
.input-group.inline-input-group .input-group.inline-input-group
%span.input-group-addon from %span.input-group-addon from
= text_field_tag :from, params[:from], class: "form-control js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from].presence } = hidden_field_tag :from, params[:from]
= button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
.dropdown-toggle-text= params[:from] || 'Select branch/tag'
= render "ref_dropdown" = render "ref_dropdown"
= "..." .compare-ellipsis ...
.form-group.dropdown.compare-form-group.js-compare-to-dropdown .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown
.input-group.inline-input-group .input-group.inline-input-group
%span.input-group-addon to %span.input-group-addon to
= text_field_tag :to, params[:to], class: "form-control js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to].presence } = hidden_field_tag :to, params[:to]
= button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
.dropdown-toggle-text= params[:to] || 'Select branch/tag'
= render "ref_dropdown" = render "ref_dropdown"
&nbsp; &nbsp;
= button_tag "Compare", class: "btn btn-create commits-compare-btn" = button_tag "Compare", class: "btn btn-create commits-compare-btn"
......
.dropdown-menu.dropdown-menu-selectable .dropdown-menu.dropdown-menu-selectable
= dropdown_title "Select branch/tag" = dropdown_title "Select branch/tag"
= dropdown_filter "Filter by branch/tag"
= dropdown_content = dropdown_content
= dropdown_loading = dropdown_loading
...@@ -11,7 +11,9 @@ ...@@ -11,7 +11,9 @@
- elsif diff_file.collapsed? - elsif diff_file.collapsed?
- url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path)) - url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path))
.nothing-here-block.diff-collapsed{data: { diff_for_path: url } } .nothing-here-block.diff-collapsed{data: { diff_for_path: url } }
This diff is collapsed. Click to expand it. This diff is collapsed.
%a.click-to-expand
Click to expand it.
- elsif diff_file.diff_lines.length > 0 - elsif diff_file.diff_lines.length > 0
- if diff_view == :parallel - if diff_view == :parallel
= render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob
......
%i.fa.diff-toggle-caret
- if defined?(blob) && blob && diff_file.submodule? - if defined?(blob) && blob && diff_file.submodule?
%span %span
= icon('archive fw') = icon('archive fw')
......
...@@ -23,6 +23,8 @@ ...@@ -23,6 +23,8 @@
or a or a
= link_to '.gitignore', add_special_file_path(@project, file_name: '.gitignore'), class: 'underlined-link' = link_to '.gitignore', add_special_file_path(@project, file_name: '.gitignore'), class: 'underlined-link'
to this project. to this project.
%p
You will need to be owner or have the master permission level for the initial push, as the master branch is automatically protected.
- if can?(current_user, :push_code, @project) - if can?(current_user, :push_code, @project)
%div{ class: container_class } %div{ class: container_class }
......
.scrolling-tabs-container.sub-nav-scroll = content_for :sub_nav do
= render 'shared/nav_scroll' .scrolling-tabs-container.sub-nav-scroll
.nav-links.sub-nav.scrolling-tabs = render 'shared/nav_scroll'
%ul{ class: (container_class) } .nav-links.sub-nav.scrolling-tabs
%ul{ class: (container_class) }
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/chart.js') = page_specific_javascript_tag('lib/chart.js')
= page_specific_javascript_tag('graphs/graphs_bundle.js') = page_specific_javascript_tag('graphs/graphs_bundle.js')
= nav_link(action: :show) do = nav_link(action: :show) do
= link_to 'Contributors', namespace_project_graph_path = link_to 'Contributors', namespace_project_graph_path
= nav_link(action: :commits) do = nav_link(action: :commits) do
= link_to 'Commits', commits_namespace_project_graph_path = link_to 'Commits', commits_namespace_project_graph_path
= nav_link(action: :languages) do = nav_link(action: :languages) do
= link_to 'Languages', languages_namespace_project_graph_path = link_to 'Languages', languages_namespace_project_graph_path
- if @project.feature_available?(:builds, current_user) - if @project.feature_available?(:builds, current_user)
= nav_link(action: :ci) do = nav_link(action: :ci) do
= link_to ci_namespace_project_graph_path do = link_to ci_namespace_project_graph_path do
Continuous Integration Continuous Integration
.scrolling-tabs-container.sub-nav-scroll = content_for :sub_nav do
= render 'shared/nav_scroll' .scrolling-tabs-container.sub-nav-scroll
.nav-links.sub-nav.scrolling-tabs = render 'shared/nav_scroll'
%ul{ class: (container_class) } .nav-links.sub-nav.scrolling-tabs
- if project_nav_tab?(:issues) && !current_controller?(:merge_requests) %ul{ class: (container_class) }
= nav_link(controller: :issues) do - if project_nav_tab?(:issues) && !current_controller?(:merge_requests)
= link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do = nav_link(controller: :issues) do
%span = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do
Issues %span
Issues
= nav_link(controller: :boards) do = nav_link(controller: :boards) do
= link_to namespace_project_board_path(@project.namespace, @project), title: 'Board' do = link_to namespace_project_board_path(@project.namespace, @project), title: 'Board' do
%span %span
Board Board
- if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests) - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests)
= nav_link(controller: :merge_requests) do = nav_link(controller: :merge_requests) do
= link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do
%span %span
Merge Requests Merge Requests
- if project_nav_tab? :labels - if project_nav_tab? :labels
= nav_link(controller: :labels) do = nav_link(controller: :labels) do
= link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
%span %span
Labels Labels
- if project_nav_tab? :milestones - if project_nav_tab? :milestones
= nav_link(controller: :milestones) do = nav_link(controller: :milestones) do
= link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
%span %span
Milestones Milestones
\ No newline at end of file
...@@ -3,7 +3,8 @@ ...@@ -3,7 +3,8 @@
- page_title "Issues" - page_title "Issues"
- new_issue_email = @project.new_issue_address(current_user) - new_issue_email = @project.new_issue_address(current_user)
= render "projects/issues/head" = content_for :sub_nav do
= render "projects/issues/head"
= content_for :meta_tags do = content_for :meta_tags do
- if current_user - if current_user
......
...@@ -72,7 +72,7 @@ ...@@ -72,7 +72,7 @@
= link_to "#", class: 'btn js-toggle-button import_git' do = link_to "#", class: 'btn js-toggle-button import_git' do
= icon('git', text: 'Repo by URL') = icon('git', text: 'Repo by URL')
%div{ class: 'import_gitlab_project' } %div{ class: 'import_gitlab_project' }
- if gitlab_project_import_enabled? && current_user.is_admin? - if gitlab_project_import_enabled?
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= icon('gitlab', text: 'GitLab export') = icon('gitlab', text: 'GitLab export')
......
.scrolling-tabs-container.sub-nav-scroll = content_for :sub_nav do
= render 'shared/nav_scroll' .scrolling-tabs-container.sub-nav-scroll
.nav-links.sub-nav.scrolling-tabs = render 'shared/nav_scroll'
%ul{ class: (container_class) } .nav-links.sub-nav.scrolling-tabs
- if project_nav_tab? :pipelines %ul{ class: (container_class) }
= nav_link(controller: :pipelines) do - if project_nav_tab? :pipelines
= link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do = nav_link(controller: :pipelines) do
%span = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
Pipelines %span
Pipelines
- if project_nav_tab? :builds - if project_nav_tab? :builds
= nav_link(controller: %w(builds)) do = nav_link(controller: %w(builds)) do
= link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
%span %span
Builds Builds
- if project_nav_tab? :environments - if project_nav_tab? :environments
= nav_link(controller: %w(environments)) do = nav_link(controller: %w(environments)) do
= link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
%span %span
Environments Environments
- if can?(current_user, :read_cycle_analytics, @project) - if can?(current_user, :read_cycle_analytics, @project)
= nav_link(controller: %w(cycle_analytics)) do = nav_link(controller: %w(cycle_analytics)) do
= link_to project_cycle_analytics_path(@project), title: 'Cycle Analytics' do = link_to project_cycle_analytics_path(@project), title: 'Cycle Analytics' do
%span %span
Cycle Analytics Cycle Analytics
...@@ -4,8 +4,8 @@ ...@@ -4,8 +4,8 @@
= content_for :meta_tags do = content_for :meta_tags do
- if current_user - if current_user
= auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits") = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits")
= render 'projects/last_push'
= render "projects/commits/head" = render "projects/commits/head"
= render 'projects/last_push'
%div{ class: container_class } %div{ class: container_class }
.tree-controls .tree-controls
......
.scrolling-tabs-container.sub-nav-scroll = content_for :sub_nav do
= render 'shared/nav_scroll' .scrolling-tabs-container.sub-nav-scroll
.nav-links.sub-nav.scrolling-tabs = render 'shared/nav_scroll'
%ul{ class: (container_class) } .nav-links.sub-nav.scrolling-tabs
= nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do %ul{ class: (container_class) }
= link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home) = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
= link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
= nav_link(path: 'wikis#pages') do = nav_link(path: 'wikis#pages') do
= link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project) = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project)
= nav_link(path: 'wikis#git_access') do = nav_link(path: 'wikis#git_access') do
= link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
Git Access Git Access
= render 'projects/wikis/new' = render 'projects/wikis/new'
...@@ -8,26 +8,26 @@ ...@@ -8,26 +8,26 @@
%b.caret %b.caret
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
%li %li
= link_to page_filter_path(sort: sort_value_priority) do = link_to page_filter_path(sort: sort_value_priority, label: true) do
= sort_title_priority = sort_title_priority
= link_to page_filter_path(sort: sort_value_recently_created) do = link_to page_filter_path(sort: sort_value_recently_created, label: true) do
= sort_title_recently_created = sort_title_recently_created
= link_to page_filter_path(sort: sort_value_oldest_created) do = link_to page_filter_path(sort: sort_value_oldest_created, label: true) do
= sort_title_oldest_created = sort_title_oldest_created
= link_to page_filter_path(sort: sort_value_recently_updated) do = link_to page_filter_path(sort: sort_value_recently_updated, label: true) do
= sort_title_recently_updated = sort_title_recently_updated
= link_to page_filter_path(sort: sort_value_oldest_updated) do = link_to page_filter_path(sort: sort_value_oldest_updated, label: true) do
= sort_title_oldest_updated = sort_title_oldest_updated
= link_to page_filter_path(sort: sort_value_milestone_soon) do = link_to page_filter_path(sort: sort_value_milestone_soon, label: true) do
= sort_title_milestone_soon = sort_title_milestone_soon
= link_to page_filter_path(sort: sort_value_milestone_later) do = link_to page_filter_path(sort: sort_value_milestone_later, label: true) do
= sort_title_milestone_later = sort_title_milestone_later
- if controller.controller_name == 'issues' || controller.action_name == 'issues' - if controller.controller_name == 'issues' || controller.action_name == 'issues'
= link_to page_filter_path(sort: sort_value_due_date_soon) do = link_to page_filter_path(sort: sort_value_due_date_soon, label: true) do
= sort_title_due_date_soon = sort_title_due_date_soon
= link_to page_filter_path(sort: sort_value_due_date_later) do = link_to page_filter_path(sort: sort_value_due_date_later, label: true) do
= sort_title_due_date_later = sort_title_due_date_later
= link_to page_filter_path(sort: sort_value_upvotes) do = link_to page_filter_path(sort: sort_value_upvotes, label: true) do
= sort_title_upvotes = sort_title_upvotes
= link_to page_filter_path(sort: sort_value_downvotes) do = link_to page_filter_path(sort: sort_value_downvotes, label: true) do
= sort_title_downvotes = sort_title_downvotes
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
- if can_change_visibility_level - if can_change_visibility_level
= render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model) = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model)
- else - else
.col-sm-10 %div
%span.info %span.info
= visibility_level_icon(visibility_level) = visibility_level_icon(visibility_level)
%strong %strong
......
...@@ -10,6 +10,6 @@ ...@@ -10,6 +10,6 @@
.option-descr .option-descr
= visibility_level_description(level, form_model) = visibility_level_description(level, form_model)
- unless restricted_visibility_levels.empty? - unless restricted_visibility_levels.empty?
.col-sm-10 %div
%span.info %span.info
Some visibility level settings have been restricted by the administrator. Some visibility level settings have been restricted by the administrator.
<svg width="249" height="368" viewBox="891 156 249 368" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="131" height="162" rx="10"/><mask id="e" x="0" y="0" width="131" height="162" fill="#fff"><use xlink:href="#a"/></mask><path d="M223.616 127.958V108.96c0-4.416-3.584-8-8.005-8h-23.985c-2.778 0-5.98 2.014-7.18 4.5l-5.07 10.5h-49.763c-5.527 0-9.996 4.475-9.996 9.997v53.005c0 5.513 4.475 9.997 9.996 9.997h84.01c5.525 0 9.994-4.477 9.994-9.998v-51.004z" id="b"/><mask id="f" x="0" y="0" width="104" height="88" fill="#fff"><use xlink:href="#b"/></mask><path d="M47 25h.996C53.52 25 58 29.472 58 34.99v20.02C58 60.526 53.52 65 47.996 65H10.004C4.48 65 0 60.528 0 55.01V34.99C0 29.474 4.48 25 10.004 25H11v-7c0-9.94 8.06-18 18-18s18 8.06 18 18v7zm-6 0H17v-7c0-6.627 5.373-12 12-12s12 5.373 12 12v7z" id="c"/><mask id="g" x="0" y="0" width="58" height="65" fill="#fff"><use xlink:href="#c"/></mask><path d="M0 10.008C0 4.48 4.476 0 10 0h218c5.523 0 10 4.473 10 10.008v140.94c0 5.53-4.062 11.882-9.08 14.196l-100.84 46.5c-5.015 2.31-13.142 2.312-18.16 0l-100.84-46.5C4.064 162.832 0 156.484 0 150.95V10.007z" id="d"/><mask id="h" x="0" y="0" width="238" height="213.417" fill="#fff"><use xlink:href="#d"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(891 156)"><g transform="rotate(8 -266.528 490.3)"><use stroke="#E5E5E5" mask="url(#e)" stroke-width="8" fill="#FFF" xlink:href="#a"/><rect fill="#FC8A51" x="20" y="31" width="12" height="4" rx="2"/><rect fill="#FC8A51" x="60" y="31" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="36" y="31" width="20" height="4" rx="2"/><rect fill="#6B4FBB" x="20" y="65" width="20" height="4" rx="2"/><rect fill="#FDE5D8" x="44" y="65" width="20" height="4" rx="2"/><rect fill="#FC8A51" x="36" y="80" width="20" height="4" rx="2"/><rect fill="#FDE5D8" x="20" y="80" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="20" y="48" width="12" height="4" rx="2"/><rect fill="#FC8A51" x="36" y="48" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="60" y="80" width="12" height="4" rx="2"/><rect fill="#6B4FBB" x="52" y="48" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="68" y="48" width="12" height="4" rx="2"/></g><use stroke="#B5A7DD" mask="url(#f)" stroke-width="8" fill="#FFF" transform="rotate(5 171.616 144.96)" xlink:href="#b"/><path d="M58 132c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#C1E7D0"/><path d="M90.143 132c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M74.686 133.875l-3.18-3.18c-.29-.29-.77-.296-1.06-.005l-1.55 1.55c-.287.287-.29.766.004 1.06l4.92 4.92c.504.504 1.32.504 1.823 0l.654-.653 7.804-7.804c.3-.3.29-.77-.005-1.067l-1.578-1.58c-.302-.3-.775-.298-1.068-.004l-6.764 6.763z" fill="#31AF64"/><path d="M4 66c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18S4 75.94 4 66z" fill="#D5ECF7"/><path d="M36.143 66c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M22 55.714c5.68 0 10.286 4.605 10.286 10.286 0 5.68-4.605 10.286-10.286 10.286-3.45 0-6.505-1.7-8.37-4.307L22 66V55.714z" fill="#2D9FD8"/><g transform="rotate(-8 748.533 18.147)"><use stroke="#FDE5D8" mask="url(#g)" stroke-width="8" fill="#FFF" xlink:href="#c"/><path d="M31 46.584c1.766-.772 3-2.534 3-4.584 0-2.76-2.24-5-5-5s-5 2.24-5 5c0 2.05 1.234 3.812 3 4.584v3.42c0 1.1.895 1.996 2 1.996 1.112 0 2-.894 2-1.997v-3.42z" fill="#FC8A51"/></g><g transform="translate(0 154)"><use stroke="#E5E5E5" mask="url(#h)" stroke-width="8" fill="#FFF" xlink:href="#d"/><g opacity=".3"><path d="M141.837 104.53l-2.56-7.993-5.074-15.843c-.26-.815-1.398-.815-1.66 0l-5.074 15.843h-16.85l-5.075-15.843c-.26-.815-1.398-.815-1.66 0l-5.073 15.843-2.56 7.993c-.234.73.022 1.528.633 1.98l22.16 16.33 22.16-16.33c.61-.452.866-1.25.632-1.98" fill="#A1A1A1"/><path fill="#5C5C5C" d="M119.044 122.84l8.425-26.303h-16.85l8.424 26.304"/><path fill="#787878" d="M119.044 122.84l-8.425-26.303H98.81l20.232 26.304"/><path fill="#787878" d="M119.044 122.84l8.425-26.303h11.807l-20.233 26.304"/><path d="M98.812 96.537l-2.56 7.993c-.234.73.022 1.528.633 1.98l22.16 16.33L98.81 96.538z" fill="#A1A1A1"/><path d="M98.812 96.537h11.807l-5.075-15.843c-.26-.815-1.398-.815-1.66 0l-5.073 15.843z" fill="#5C5C5C"/><path d="M139.277 96.537l2.56 7.993c.234.73-.022 1.528-.634 1.98l-22.16 16.33 20.234-26.303z" fill="#A1A1A1"/><path d="M139.277 96.537H127.47l5.074-15.843c.26-.815 1.398-.815 1.66 0l5.073 15.843z" fill="#5C5C5C"/></g><path d="M57 18.29c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83H41c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83H77c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm17 24.693c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V28.35c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.633zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V28.35c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.633zm202 32.923c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V61.274c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V61.274c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm202 32.923c0 1.01.895 1.828 2 1.828s2-.82 2-1.83V94.2c0-1.012-.895-1.83-2-1.83s-2 .818-2 1.83v14.63zm-202 0c0 1.01.895 1.828 2 1.828s2-.82 2-1.83V94.2c0-1.012-.895-1.83-2-1.83s-2 .818-2 1.83v14.63zm202 32.922c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V127.12c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V127.12c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm179.023 19.555c-.988.452-1.388 1.55-.894 2.454.493.904 1.694 1.27 2.682.82l14.31-6.545c.99-.452 1.39-1.55.896-2.454-.494-.902-1.696-1.27-2.684-.817l-14.31 6.544zm-32.2 14.723c-.987.452-1.388 1.55-.894 2.454.493.904 1.695 1.27 2.683.818l14.31-6.544c.99-.45 1.39-1.55.895-2.454-.494-.903-1.695-1.27-2.683-.818l-14.31 6.544zm-32.2 14.724c-.987.45-1.387 1.55-.893 2.454.494.903 1.695 1.27 2.683.818l14.31-6.544c.99-.452 1.39-1.55.896-2.454-.495-.904-1.697-1.27-2.685-.818l-14.31 6.544zm-23.67-2.023l-12.186-5.57c-.987-.452-2.19-.086-2.683.817-.494.904-.093 2.003.895 2.454l12.185 5.573c.754.345 1.57.645 2.438.898 1.052.307 2.177-.224 2.513-1.187.335-.962-.246-1.99-1.298-2.298-.677-.197-1.302-.426-1.864-.684zM62.57 168.437c-.988-.452-2.19-.086-2.683.818-.494.903-.094 2.002.894 2.454l14.31 6.544c.988.45 2.19.085 2.683-.818.494-.904.094-2.003-.894-2.454l-14.312-6.544zm-32.2-14.723c-.988-.452-2.19-.086-2.683.818-.494.904-.093 2.003.895 2.454l14.31 6.544c.988.452 2.19.086 2.684-.818.494-.903.093-2.002-.895-2.454l-14.312-6.543z" fill="#EEE"/></g><g><path d="M104 18c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#FADFD9"/><path d="M136.143 18c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M119.43 8.994c0-.707.57-1.28 1.283-1.28h2.574c.71 0 1.284.57 1.284 1.28v10.298c0 .706-.57 1.28-1.283 1.28h-2.574c-.71 0-1.284-.57-1.284-1.28V8.994zm0 15.433c0-.71.57-1.284 1.283-1.284h2.574c.71 0 1.284.57 1.284 1.284V27c0 .71-.57 1.286-1.283 1.286h-2.574c-.71 0-1.284-.57-1.284-1.285v-2.573z" fill="#E75E40"/></g><g><path d="M213 89c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#F6D4DC"/><path d="M245.143 89c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M231 86.348l-3.603-3.602c-.288-.29-.766-.286-1.063.01l-1.578 1.578c-.3.302-.3.773-.01 1.063L228.348 89l-3.602 3.603c-.29.288-.286.766.01 1.063l1.578 1.578c.302.3.773.3 1.063.01L231 91.652l3.603 3.602c.288.29.766.286 1.063-.01l1.578-1.578c.3-.302.3-.773.01-1.063L233.652 89l3.602-3.603c.29-.288.286-.766-.01-1.063l-1.578-1.578c-.302-.3-.773-.3-1.063-.01L231 86.348z" fill="#D22852"/></g></g></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="m6 11h-4.509c-.263 0-.491.226-.491.505v.991c0 .291.22.505.491.505h4.509v.679c0 .301.194.413.454.236l2.355-1.607c.251-.171.259-.442 0-.619l-2.355-1.607c-.251-.171-.454-.07-.454.236v.681m-5-7.495c0-.279.22-.505.498-.505h13c.275 0 .498.214.498.505v.991c0 .279-.22.505-.498.505h-13c-.275 0-.498-.214-.498-.505v-.991m10 8c0-.279.215-.505.49-.505h3.02c.271 0 .49.214.49.505v.991c0 .279-.215.505-.49.505h-3.02c-.271 0-.49-.214-.49-.505v-.991m-10-4c0-.279.22-.505.498-.505h13c.275 0 .498.214.498.505v.991c0 .279-.22.505-.498.505h-13c-.275 0-.498-.214-.498-.505v-.991"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="m12 11h-2v-.681c0-.307-.203-.407-.454-.236l-2.355 1.607c-.259.177-.251.448 0 .619l2.355 1.607c.259.177.454.065.454-.236v-.679h2c0 0 0 0 0 0 1.657 0 3-1.343 3-3 0-.828-.336-1.578-.879-2.121-.543-.543-1.293-.879-2.121-.879-.001 0-.002 0-.002 0h-10.497c-.271 0-.5.226-.5.505v.991c0 .291.224.505.5.505h10.497c.001 0 .002 0 .002 0 .552 0 1 .448 1 1 0 .276-.112.526-.293.707-.181.181-.431.293-.707.293m-11-7.495c0-.279.22-.505.498-.505h13c.275 0 .498.214.498.505v.991c0 .279-.22.505-.498.505h-13c-.275 0-.498-.214-.498-.505v-.991m0 8c0-.279.215-.505.49-.505h3.02c.271 0 .49.214.49.505v.991c0 .279-.215.505-.49.505h-3.02c-.271 0-.49-.214-.49-.505v-.991"/>
</svg>
- type = local_assigns.fetch(:type, :issues)
- page_context_word = type.to_s.humanize(capitalize: false)
- issuables = @issues || @merge_requests
%ul.nav-links.issues-state-filters %ul.nav-links.issues-state-filters
- if defined?(type) && type == :merge_requests
- page_context_word = 'merge requests'
- else
- page_context_word = 'issues'
%li{class: ("active" if params[:state] == 'opened')} %li{class: ("active" if params[:state] == 'opened')}
= link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do = link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do
#{state_filters_text_for(:opened, @project)} #{issuables_state_counter_text(type, :opened)}
- if defined?(type) && type == :merge_requests - if type == :merge_requests
%li{class: ("active" if params[:state] == 'merged')} %li{class: ("active" if params[:state] == 'merged')}
= link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do = link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do
#{state_filters_text_for(:merged, @project)} #{issuables_state_counter_text(type, :merged)}
%li{class: ("active" if params[:state] == 'closed')} %li{class: ("active" if params[:state] == 'closed')}
= link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do
#{state_filters_text_for(:closed, @project)} #{issuables_state_counter_text(type, :closed)}
- else - else
%li{class: ("active" if params[:state] == 'closed')} %li{class: ("active" if params[:state] == 'closed')}
= link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do
#{state_filters_text_for(:closed, @project)} #{issuables_state_counter_text(type, :closed)}
%li{class: ("active" if params[:state] == 'all')} %li{class: ("active" if params[:state] == 'all')}
= link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do = link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do
#{state_filters_text_for(:all, @project)} #{issuables_state_counter_text(type, :all)}
# Make sure we initialize a Redis connection pool before Sidekiq starts
# multi-threaded execution.
Gitlab::Redis.with { nil }
module AttrEncrypted module AttrEncrypted
module Adapters module Adapters
module ActiveRecord module ActiveRecord
def attribute_instance_methods_as_symbols_with_no_db_connection module DBConnectionQuerier
# Use with_connection so the connection doesn't stay pinned to the thread. def attribute_instance_methods_as_symbols
connected = ::ActiveRecord::Base.connection_pool.with_connection(&:active?) rescue false # Use with_connection so the connection doesn't stay pinned to the thread.
connected = ::ActiveRecord::Base.connection_pool.with_connection(&:active?) rescue false
if connected
# Call version from AttrEncrypted::Adapters::ActiveRecord if connected
attribute_instance_methods_as_symbols_without_no_db_connection # Call version from AttrEncrypted::Adapters::ActiveRecord
else super
# Call version from AttrEncrypted, i.e., `super` with regards to AttrEncrypted::Adapters::ActiveRecord else
AttrEncrypted.instance_method(:attribute_instance_methods_as_symbols).bind(self).call # Call version from AttrEncrypted, i.e., `super` with regards to AttrEncrypted::Adapters::ActiveRecord
AttrEncrypted.instance_method(:attribute_instance_methods_as_symbols).bind(self).call
end
end end
end end
prepend DBConnectionQuerier
alias_method_chain :attribute_instance_methods_as_symbols, :no_db_connection
end end
end end
end end
if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
module LimitFilter
def add_column(table_name, column_name, type, options = {})
options.delete(:limit) if type == :text
super(table_name, column_name, type, options)
end
def change_column(table_name, column_name, type, options = {})
options.delete(:limit) if type == :text
super(table_name, column_name, type, options)
end
end
prepend ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::LimitFilter
class TableDefinition class TableDefinition
def text(*args) def text(*args)
options = args.extract_options! options = args.extract_options!
...@@ -9,18 +23,5 @@ if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) ...@@ -9,18 +23,5 @@ if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
column_names.each { |name| column(name, type, options) } column_names.each { |name| column(name, type, options) }
end end
end end
def add_column_with_limit_filter(table_name, column_name, type, options = {})
options.delete(:limit) if type == :text
add_column_without_limit_filter(table_name, column_name, type, options)
end
def change_column_with_limit_filter(table_name, column_name, type, options = {})
options.delete(:limit) if type == :text
change_column_without_limit_filter(table_name, column_name, type, options)
end
alias_method_chain :add_column, :limit_filter
alias_method_chain :change_column, :limit_filter
end end
end end
Gitlab::Seeder.quiet do Gitlab::Seeder.quiet do
Group.all.each do |group| Group.all.each do |group|
User.all.sample(4).each do |user| User.all.sample(4).each do |user|
if group.add_users([user.id], Gitlab::Access.values.sample) if group.add_user(user, Gitlab::Access.values.sample).persisted?
print '.' print '.'
else else
print 'F' print 'F'
......
...@@ -101,7 +101,7 @@ Once you have your token, pass it to the API using either the `private_token` ...@@ -101,7 +101,7 @@ Once you have your token, pass it to the API using either the `private_token`
parameter or the `PRIVATE-TOKEN` header. parameter or the `PRIVATE-TOKEN` header.
### Session cookie ### Session Cookie
When signing in to GitLab as an ordinary user, a `_gitlab_session` cookie is When signing in to GitLab as an ordinary user, a `_gitlab_session` cookie is
set. The API will use this cookie for authentication if it is present, but using set. The API will use this cookie for authentication if it is present, but using
......
# GitLab as an OAuth2 client # GitLab as an OAuth2 provider
This document covers using the OAuth2 protocol to access GitLab. This document covers using the OAuth2 protocol to access GitLab.
...@@ -112,7 +112,7 @@ You can do POST request to `/oauth/token` with parameters: ...@@ -112,7 +112,7 @@ You can do POST request to `/oauth/token` with parameters:
{ {
"grant_type" : "password", "grant_type" : "password",
"username" : "user@example.com", "username" : "user@example.com",
"password" : "sekret" "password" : "secret"
} }
``` ```
...@@ -130,8 +130,8 @@ For testing you can use the oauth2 ruby gem: ...@@ -130,8 +130,8 @@ For testing you can use the oauth2 ruby gem:
``` ```
client = OAuth2::Client.new('the_client_id', 'the_client_secret', :site => "http://example.com") client = OAuth2::Client.new('the_client_id', 'the_client_secret', :site => "http://example.com")
access_token = client.password.get_token('user@example.com', 'sekret') access_token = client.password.get_token('user@example.com', 'secret')
puts access_token.token puts access_token.token
``` ```
[personal access tokens]: ./README.md#personal-access-tokens [personal access tokens]: ./README.md#personal-access-tokens
\ No newline at end of file
...@@ -41,7 +41,9 @@ Example response: ...@@ -41,7 +41,9 @@ Example response:
"gravatar_enabled" : true, "gravatar_enabled" : true,
"sign_in_text" : null, "sign_in_text" : null,
"container_registry_token_expire_delay": 5, "container_registry_token_expire_delay": 5,
"repository_storage": "default" "repository_storage": "default",
"koding_enabled": false,
"koding_url": null
} }
``` ```
...@@ -72,7 +74,9 @@ PUT /application/settings ...@@ -72,7 +74,9 @@ PUT /application/settings
| `after_sign_out_path` | string | no | Where to redirect users after logout | | `after_sign_out_path` | string | no | Where to redirect users after logout |
| `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes | | `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes |
| `repository_storage` | string | no | Storage path for new projects. The value should be the name of one of the repository storage paths defined in your gitlab.yml | | `repository_storage` | string | no | Storage path for new projects. The value should be the name of one of the repository storage paths defined in your gitlab.yml |
| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. | `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. |
| `koding_enabled` | boolean | no | Enable Koding integration. Default is `false`. |
| `koding_url` | string | yes (if `koding_enabled` is `true`) | The Koding instance URL for integration. |
```bash ```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1 curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1
...@@ -103,6 +107,8 @@ Example response: ...@@ -103,6 +107,8 @@ Example response:
"user_oauth_applications": true, "user_oauth_applications": true,
"after_sign_out_path": "", "after_sign_out_path": "",
"container_registry_token_expire_delay": 5, "container_registry_token_expire_delay": 5,
"repository_storage": "default" "repository_storage": "default",
"koding_enabled": false,
"koding_url": null
} }
``` ```
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
> **Note**: > **Note**:
GitLab 8.12 has a completely redesigned build permissions system. GitLab 8.12 has a completely redesigned build permissions system.
Read all about the [new model and its implications][../../user/project/new_ci_build_permissions_model.md#build-triggers]. Read all about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#build-triggers).
Triggers can be used to force a rebuild of a specific branch, tag or commit, Triggers can be used to force a rebuild of a specific branch, tag or commit,
with an API call. with an API call.
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
- [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations - [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations
- [Testing standards and style guidelines](testing.md) - [Testing standards and style guidelines](testing.md)
- [UI guide](ui_guide.md) for building GitLab with existing CSS styles and elements - [UI guide](ui_guide.md) for building GitLab with existing CSS styles and elements
- [Frontend guidelines](frontend.md)
- [SQL guidelines](sql.md) for SQL guidelines - [SQL guidelines](sql.md) for SQL guidelines
## Process ## Process
......
# Frontend Development Guidelines
This document describes various guidelines to ensure consistency and quality
across GitLab's frontend team.
## Overview
GitLab is built on top of [Ruby on Rails][rails] using [Haml][haml] with
[Hamlit][hamlit]. Be wary of [the limitations that come with using
Hamlit][hamlit-limits]. We also use [SCSS][scss] and plain JavaScript with
[ES6 by way of Babel][es6].
The asset pipeline is [Sprockets][sprockets], which handles the concatenation,
minification, and compression of our assets.
[jQuery][jquery] is used throughout the application's JavaScript, with
[Vue.js][vue] for particularly advanced, dynamic elements.
### Vue
For more complex frontend features, we recommend using Vue.js. It shares
some ideas with React.js as well as Angular.
To get started with Vue, read through [their documentation][vue-docs].
## Performance
### Resources
- [WebPage Test][web-page-test] for testing site loading time and size.
- [Google PageSpeed Insights][pagespeed-insights] grades web pages and provides feedback to improve the page.
- [Profiling with Chrome DevTools][google-devtools-profiling]
- [Browser Diet][browser-diet] is a community-built guide that catalogues practical tips for improving web page performance.
### Page-specific JavaScript
Certain pages may require the use of a third party library, such as [d3][d3] for
the User Activity Calendar and [Chart.js][chartjs] for the Graphs pages. These
libraries increase the page size significantly, and impact load times due to
bandwidth bottlenecks and the browser needing to parse more JavaScript.
In cases where libraries are only used on a few specific pages, we use
"page-specific JavaScript" to prevent the main `application.js` file from
becoming unnecessarily large.
Steps to split page-specific JavaScript from the main `application.js`:
1. Create a directory for the specific page(s), e.g. `graphs/`.
1. In that directory, create a `namespace_bundle.js` file, e.g. `graphs_bundle.js`.
1. In `graphs_bundle.js` add the line `//= require_tree .`, this adds all other files in the directory to the bundle.
1. Add any necessary libraries to `app/assets/javascripts/lib/`, all files directly descendant from this directory will be precompiled as separate assets, in this case `chart.js` would be added.
1. Add the new "bundle" file to the list of precompiled assets in
`config/application.rb`.
- For example: `config.assets.precompile << "graphs/graphs_bundle.js"`.
1. Move code reliant on these libraries into the `graphs` directory.
1. In the relevant views, add the scripts to the page with the following:
```haml
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/chart.js')
= page_specific_javascript_tag('graphs/graphs_bundle.js')
```
The above loads `chart.js` and `graphs_bundle.js` for this page only. `chart.js`
is separated from the bundle file so it can be cached separately from the bundle
and reused for other pages that also rely on the library. For an example, see
[this Haml file][page-specific-js-example].
### Minimizing page size
A smaller page size means the page loads faster (especially important on mobile
and poor connections), the page is parsed more quickly by the browser, and less
data is used for users with capped data plans.
General tips:
- Don't add new fonts.
- Prefer font formats with better compression, e.g. WOFF2 is better than WOFF, which is better than TTF.
- Compress and minify assets wherever possible (For CSS/JS, Sprockets does this for us).
- If some functionality can reasonably be achieved without adding extra libraries, avoid them.
- Use page-specific JavaScript as described above to dynamically load libraries that are only needed on certain pages.
## Accessibility
### Resources
[Chrome Accessibility Developer Tools][chrome-accessibility-developer-tools]
are useful for testing for potential accessibility problems in GitLab.
Accessibility best-practices and more in-depth information is available on
[the Audit Rules page][audit-rules] for the Chrome Accessibility Developer Tools.
## Security
### Resources
[Mozilla’s HTTP Observatory CLI][observatory-cli] and the
[Qualys SSL Labs Server Test][qualys-ssl] are good resources for finding
potential problems and ensuring compliance with security best practices.
<!-- Uncomment these sections when CSP/SRI are implemented.
### Content Security Policy (CSP)
Content Security Policy is a web standard that intends to mitigate certain
forms of Cross-Site Scripting (XSS) as well as data injection.
Content Security Policy rules should be taken into consideration when
implementing new features, especially those that may rely on connection with
external services.
GitLab's CSP is used for the following:
- Blocking plugins like Flash and Silverlight from running at all on our pages.
- Blocking the use of scripts and stylesheets downloaded from external sources.
- Upgrading `http` requests to `https` when possible.
- Preventing `iframe` elements from loading in most contexts.
Some exceptions include:
- Scripts from Google Analytics and Piwik if either is enabled.
- Connecting with GitHub, Bitbucket, GitLab.com, etc. to allow project importing.
- Connecting with Google, Twitter, GitHub, etc. to allow OAuth authentication.
We use [the Secure Headers gem][secure_headers] to enable Content
Security Policy headers in the GitLab Rails app.
Some resources on implementing Content Security Policy:
- [MDN Article on CSP][mdn-csp]
- [GitHub’s CSP Journey on the GitHub Engineering Blog][github-eng-csp]
- The Dropbox Engineering Blog's series on CSP: [1][dropbox-csp-1], [2][dropbox-csp-2], [3][dropbox-csp-3], [4][dropbox-csp-4]
### Subresource Integrity (SRI)
Subresource Integrity prevents malicious assets from being provided by a CDN by
guaranteeing that the asset downloaded is identical to the asset the server
is expecting.
The Rails app generates a unique hash of the asset, which is used as the
asset's `integrity` attribute. The browser generates the hash of the asset
on-load and will reject the asset if the hashes do not match.
All CSS and JavaScript assets should use Subresource Integrity. For implementation details,
see the documentation for [the Sprockets implementation of SRI][sprockets-sri].
Some resources on implementing Subresource Integrity:
- [MDN Article on SRI][mdn-sri]
- [Subresource Integrity on the GitHub Engineering Blog][github-eng-sri]
-->
### Including external resources
External fonts, CSS, and JavaScript should never be used with the exception of
Google Analytics and Piwik - and only when the instance has enabled it. Assets
should always be hosted and served locally from the GitLab instance. Embedded
resources via `iframes` should never be used except in certain circumstances
such as with ReCaptcha, which cannot be used without an `iframe`.
### Avoiding inline scripts and styles
In order to protect users from [XSS vulnerabilities][xss], we will disable inline scripts in the future using Content Security Policy.
While inline scripts can be useful, they're also a security concern. If
user-supplied content is unintentionally left un-sanitized, malicious users can
inject scripts into the web app.
Inline styles should be avoided in almost all cases, they should only be used
when no alternatives can be found. This allows reusability of styles as well as
readability.
## Style guides and linting
See the relevant style guides for our guidelines and for information on linting:
- [SCSS][scss-style-guide]
## Testing
Feature tests need to be written for all new features. Regression tests
also need to be written for all bug fixes to prevent them from occurring
again in the future.
See [the Testing Standards and Style Guidelines](testing.md) for more
information.
## Supported browsers
For our currently-supported browsers, see our [requirements][requirements].
[rails]: http://rubyonrails.org/
[haml]: http://haml.info/
[hamlit]: https://github.com/k0kubun/hamlit
[hamlit-limits]: https://github.com/k0kubun/hamlit/blob/master/REFERENCE.md#limitations
[scss]: http://sass-lang.com/
[es6]: https://babeljs.io/
[sprockets]: https://github.com/rails/sprockets
[jquery]: https://jquery.com/
[vue]: http://vuejs.org/
[vue-docs]: http://vuejs.org/guide/index.html
[web-page-test]: http://www.webpagetest.org/
[pagespeed-insights]: https://developers.google.com/speed/pagespeed/insights/
[google-devtools-profiling]: https://developers.google.com/web/tools/chrome-devtools/profile/?hl=en
[browser-diet]: https://browserdiet.com/
[d3]: https://d3js.org/
[chartjs]: http://www.chartjs.org/
[page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8
[chrome-accessibility-developer-tools]: https://github.com/GoogleChrome/accessibility-developer-tools
[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules
[observatory-cli]: https://github.com/mozilla/http-observatory-cli)
[qualys-ssl]: https://www.ssllabs.com/ssltest/analyze.html
[secure_headers]: https://github.com/twitter/secureheaders
[mdn-csp]: https://developer.mozilla.org/en-US/docs/Web/Security/CSP
[github-eng-csp]: http://githubengineering.com/githubs-csp-journey/
[dropbox-csp-1]: https://blogs.dropbox.com/tech/2015/09/on-csp-reporting-and-filtering/
[dropbox-csp-2]: https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/
[dropbox-csp-3]: https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/
[dropbox-csp-4]: https://blogs.dropbox.com/tech/2015/09/csp-third-party-integrations-and-privilege-separation/
[mdn-sri]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
[github-eng-sri]: http://githubengineering.com/subresource-integrity/
[sprockets-sri]: https://github.com/rails/sprockets-rails#sri-support
[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting
[scss-style-guide]: scss_styleguide.md
[requirements]: ../install/requirements.md#supported-web-browsers
...@@ -9,10 +9,10 @@ a big burden for most organizations. For this reason it is important that your ...@@ -9,10 +9,10 @@ a big burden for most organizations. For this reason it is important that your
migrations are written carefully, can be applied online and adhere to the style guide below. migrations are written carefully, can be applied online and adhere to the style guide below.
Migrations should not require GitLab installations to be taken offline unless Migrations should not require GitLab installations to be taken offline unless
_absolutely_ necessary. If a migration requires downtime this should be _absolutely_ necessary - see the ["What Requires Downtime?"](what_requires_downtime.md)
clearly mentioned during the review process as well as being documented in the page. If a migration requires downtime, this should be clearly mentioned during
monthly release post. For more information see the "Downtime Tagging" section the review process, as well as being documented in the monthly release post. For
below. more information, see the "Downtime Tagging" section below.
When writing your migrations, also consider that databases might have stale data When writing your migrations, also consider that databases might have stale data
or inconsistencies and guard for that. Try to make as little assumptions as possible or inconsistencies and guard for that. Try to make as little assumptions as possible
......
...@@ -108,7 +108,7 @@ Then select 'Internet Site' and press enter to confirm the hostname. ...@@ -108,7 +108,7 @@ Then select 'Internet Site' and press enter to confirm the hostname.
## 2. Ruby ## 2. Ruby
_**Note:** The current supported Ruby versions are 2.1.x and 2.3.x. 2.3.x is preferred, and support for 2.1.x will be dropped in the future. **Note:** The current supported Ruby versions are 2.1.x and 2.3.x. 2.3.x is preferred, and support for 2.1.x will be dropped in the future.
The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab
in production, frequently leads to hard to diagnose problems. For example, in production, frequently leads to hard to diagnose problems. For example,
...@@ -268,9 +268,9 @@ sudo usermod -aG redis git ...@@ -268,9 +268,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-12-stable gitlab sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-13-stable gitlab
**Note:** You can change `8-12-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! **Note:** You can change `8-13-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It ### Configure It
......
...@@ -22,7 +22,9 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ...@@ -22,7 +22,9 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
### 3. Update Ruby ### 3. Update Ruby
If you are you running Ruby 2.1.x, you do not _need_ to upgrade Ruby yet, but you should note that support for 2.1.x is deprecated and we will require 2.3.x in 8.13. It's strongly recommended that you upgrade as soon as possible. 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`. You can check which version you are running with `ruby -v`.
......
...@@ -22,7 +22,9 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ...@@ -22,7 +22,9 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
### 3. Update Ruby ### 3. Update Ruby
If you are you running Ruby 2.1.x, you do not _need_ to upgrade Ruby yet, but you should note that support for 2.1.x is deprecated and we will require 2.3.x in 8.13. It's strongly recommended that you upgrade as soon as possible. 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`. You can check which version you are running with `ruby -v`.
......
# From 8.12 to 8.13
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
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.1.tar.gz
echo 'c39b4001f7acb4e334cb60a0f4df72d434bef711 ruby-2.3.1.tar.gz' | shasum --check - && tar xzf ruby-2.3.1.tar.gz
cd ruby-2.3.1
./configure --disable-install-rdoc
make
sudo make install
```
Install Bundler:
```bash
sudo gem install bundler --no-ri --no-rdoc
```
### 4. Get latest code
```bash
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
sudo -u git -H git checkout 8-13-stable
```
OR
For GitLab Enterprise Edition:
```bash
sudo -u git -H git checkout 8-13-stable-ee
```
### 5. Update gitlab-shell
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
sudo -u git -H git checkout v3.6.3
```
### 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-workhorse
sudo -u git -H git fetch --all
sudo -u git -H git checkout v0.8.2
sudo -u git -H make
```
### 7. 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
```
### 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
git diff origin/8-12-stable:config/gitlab.yml.example origin/8-13-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
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
# For HTTPS configurations
git diff origin/8-12-stable:lib/support/nginx/gitlab-ssl origin/8-13-stable:lib/support/nginx/gitlab-ssl
# For HTTP configurations
git diff origin/8-12-stable:lib/support/nginx/gitlab origin/8-13-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-13-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-13-stable/config/initializers/smtp_settings.rb.sample#L13
#### Init script
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
### 9. Start application
sudo service gitlab start
sudo service nginx restart
### 10. Check application status
Check if GitLab and its environment are configured correctly:
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
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
If all items are green, then congratulations, the upgrade is complete!
## Things went south? Revert to previous version (8.12)
### 1. Revert the code to the previous version
Follow the [upgrade guide from 8.11 to 8.12](8.11-to-8.12.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.
...@@ -23,6 +23,7 @@ The following table depicts the various user permission levels in a project. ...@@ -23,6 +23,7 @@ The following table depicts the various user permission levels in a project.
| See a list of builds | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | | See a list of builds | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
| See a build log | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | | See a build log | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
| Download and browse build artifacts | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | | Download and browse build artifacts | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
| View wiki pages | ✓ | ✓ | ✓ | ✓ | ✓ |
| Pull project code | | ✓ | ✓ | ✓ | ✓ | | Pull project code | | ✓ | ✓ | ✓ | ✓ |
| Download project | | ✓ | ✓ | ✓ | ✓ | | Download project | | ✓ | ✓ | ✓ | ✓ |
| Create code snippets | | ✓ | ✓ | ✓ | ✓ | | Create code snippets | | ✓ | ✓ | ✓ | ✓ |
......
...@@ -7,7 +7,8 @@ ...@@ -7,7 +7,8 @@
> that of the exporter. > that of the exporter.
> - For existing installations, the project import option has to be enabled in > - For existing installations, the project import option has to be enabled in
> application settings (`/admin/application_settings`) under 'Import sources'. > application settings (`/admin/application_settings`) under 'Import sources'.
> You will have to be an administrator to enable and use the import functionality. > Ask your administrator if you don't see the **GitLab export** button when
> creating a new project.
> - You can find some useful raketasks if you are an administrator in the > - You can find some useful raketasks if you are an administrator in the
> [import_export](../../../administration/raketasks/project_import_export.md) > [import_export](../../../administration/raketasks/project_import_export.md)
> raketask. > raketask.
......
...@@ -27,4 +27,5 @@ do. ...@@ -27,4 +27,5 @@ do.
| `/subscribe` | Subscribe | | `/subscribe` | Subscribe |
| `/unsubscribe` | Unsubscribe | | `/unsubscribe` | Unsubscribe |
| <code>/due &lt;in 2 days &#124; this Friday &#124; December 31st&gt;</code> | Set due date | | <code>/due &lt;in 2 days &#124; this Friday &#124; December 31st&gt;</code> | Set due date |
| `/remove_due_date` | Remove due date | | `/remove_due_date` | Remove due date |
| `/wip` | Toggle the Work In Progress status |
@profile
Feature: Profile SSH Keys
Background:
Given I sign in as a user
And I have ssh key "ssh-rsa Work"
And I visit profile keys page
Scenario: I should see ssh keys
Then I should see my ssh keys
Scenario: Add new ssh key
Given I should see new ssh key form
And I submit new ssh key "Laptop"
Then I should see new ssh key "Laptop"
Scenario: Remove ssh key
Given I click link "Work"
And I click link "Remove"
Then I visit profile keys page
And I should not see "Work" ssh key
...@@ -20,6 +20,7 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps ...@@ -20,6 +20,7 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
expect(page).to have_link('GitLab.com') expect(page).to have_link('GitLab.com')
expect(page).to have_link('Google Code') expect(page).to have_link('Google Code')
expect(page).to have_link('Repo by URL') expect(page).to have_link('Repo by URL')
expect(page).to have_link('GitLab export')
end end
step 'I click on "Import project from GitHub"' do step 'I click on "Import project from GitHub"' do
......
class Spinach::Features::ProfileSshKeys < Spinach::FeatureSteps
include SharedAuthentication
step 'I should see my ssh keys' do
@user.keys.each do |key|
expect(page).to have_content(key.title)
end
end
step 'I should see new ssh key form' do
expect(page).to have_content("Add an SSH key")
end
step 'I submit new ssh key "Laptop"' do
fill_in "key_title", with: "Laptop"
fill_in "key_key", with: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop"
click_button "Add key"
end
step 'I should see new ssh key "Laptop"' do
key = Key.find_by(title: "Laptop")
expect(page).to have_content(key.title)
expect(page).to have_content(key.key)
expect(current_path).to eq profile_key_path(key)
end
step 'I click link "Work"' do
click_link "Work"
end
step 'I click link "Remove"' do
click_link "Remove"
end
step 'I visit profile keys page' do
visit profile_keys_path
end
step 'I should not see "Work" ssh key' do
expect(page).not_to have_content "Work"
end
step 'I have ssh key "ssh-rsa Work"' do
create(:key, user: @user, title: "ssh-rsa Work", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+L3TbFegm3k8QjejSwemk4HhlRh+DuN679Pc5ckqE/MPhVtE/+kZQDYCTB284GiT2aIoGzmZ8ee9TkaoejAsBwlA+Wz2Q3vhz65X6sMgalRwpdJx8kSEUYV8ZPV3MZvPo8KdNg993o4jL6G36GDW4BPIyO6FPZhfsawdf6liVD0Xo5kibIK7B9VoE178cdLQtLpS2YolRwf5yy6XR6hbbBGQR+6xrGOdP16eGZDb1CE2bMvvJijjloFqPscGktWOqW+nfh5txwFfBzlfARDTBsS8WZtg3Yoj1kn33kPsWRlgHfNutFRAIynDuDdQzQq8tTtVwm+Yi75RfcPHW8y3P Work")
end
end
...@@ -16,9 +16,9 @@ module API ...@@ -16,9 +16,9 @@ module API
# GET /projects/:id/access_requests # GET /projects/:id/access_requests
get ":id/access_requests" do get ":id/access_requests" do
source = find_source(source_type, params[:id]) source = find_source(source_type, params[:id])
authorize_admin_source!(source_type, source)
access_requesters = paginate(source.requesters.includes(:user)) access_requesters = AccessRequestsFinder.new(source).execute!(current_user)
access_requesters = paginate(access_requesters.includes(:user))
present access_requesters.map(&:user), with: Entities::AccessRequester, source: source present access_requesters.map(&:user), with: Entities::AccessRequester, source: source
end end
......
...@@ -494,6 +494,8 @@ module API ...@@ -494,6 +494,8 @@ module API
expose :after_sign_out_path expose :after_sign_out_path
expose :container_registry_token_expire_delay expose :container_registry_token_expire_delay
expose :repository_storage expose :repository_storage
expose :koding_enabled
expose :koding_url
end end
class Release < Grape::Entity class Release < Grape::Entity
......
...@@ -4,10 +4,9 @@ module API ...@@ -4,10 +4,9 @@ module API
before { authenticate! } before { authenticate! }
resource :keys do resource :keys do
# Get single ssh key by id. Only available to admin users. desc 'Get single ssh key by id. Only available to admin users' do
# success Entities::SSHKeyWithUser
# Example Request: end
# GET /keys/:id
get ":id" do get ":id" do
authenticated_as_admin! authenticated_as_admin!
......
...@@ -59,13 +59,6 @@ module API ...@@ -59,13 +59,6 @@ module API
authorize_admin_source!(source_type, source) authorize_admin_source!(source_type, source)
required_attributes! [:user_id, :access_level] required_attributes! [:user_id, :access_level]
access_requester = source.requesters.find_by(user_id: params[:user_id])
if access_requester
# We pass current_user = access_requester so that the requester doesn't
# receive a "access denied" email
::Members::DestroyService.new(access_requester, access_requester.user).execute
end
member = source.members.find_by(user_id: params[:user_id]) member = source.members.find_by(user_id: params[:user_id])
# This is to ensure back-compatibility but 409 behavior should be used # This is to ensure back-compatibility but 409 behavior should be used
...@@ -73,18 +66,12 @@ module API ...@@ -73,18 +66,12 @@ module API
conflict!('Member already exists') if source_type == 'group' && member conflict!('Member already exists') if source_type == 'group' && member
unless member unless member
source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at]) member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
member = source.members.find_by(user_id: params[:user_id])
end end
if member if member.persisted? && member.valid?
present member.user, with: Entities::Member, member: member present member.user, with: Entities::Member, member: member
else else
# Since `source.add_user` doesn't return a member object, we have to
# build a new one and populate its errors in order to render them.
member = source.members.build(attributes_for_keys([:user_id, :access_level, :expires_at]))
member.valid? # populate the errors
# This is to ensure back-compatibility but 400 behavior should be used # This is to ensure back-compatibility but 400 behavior should be used
# for all validation errors in 9.0! # for all validation errors in 9.0!
render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level) render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
......
...@@ -108,8 +108,7 @@ module API ...@@ -108,8 +108,7 @@ module API
finder_params = { finder_params = {
project_id: user_project.id, project_id: user_project.id,
milestone_title: @milestone.title, milestone_title: @milestone.title
state: 'all'
} }
issues = IssuesFinder.new(current_user, finder_params).execute issues = IssuesFinder.new(current_user, finder_params).execute
......
...@@ -10,19 +10,21 @@ module Banzai ...@@ -10,19 +10,21 @@ module Banzai
# task_list gem. # task_list gem.
# #
# See https://github.com/github/task_list/pull/60 # See https://github.com/github/task_list/pull/60
class TaskListFilter < TaskList::Filter module ClassNamesFilter
def add_css_class_with_fix(node, *new_class_names) def add_css_class(node, *new_class_names)
if new_class_names.include?('task-list') if new_class_names.include?('task-list')
# Don't add class to all lists # Don't add class to all lists
return return
elsif new_class_names.include?('task-list-item') elsif new_class_names.include?('task-list-item')
add_css_class_without_fix(node.parent, 'task-list') super(node.parent, 'task-list')
end end
add_css_class_without_fix(node, *new_class_names) super(node, *new_class_names)
end end
end
alias_method_chain :add_css_class, :fix class TaskListFilter < TaskList::Filter
prepend ClassNamesFilter
end end
end end
end end
...@@ -4,7 +4,7 @@ module Ci ...@@ -4,7 +4,7 @@ module Ci
include Gitlab::Ci::Config::Node::LegacyValidationHelpers include Gitlab::Ci::Config::Node::LegacyValidationHelpers
attr_reader :path, :cache, :stages attr_reader :path, :cache, :stages, :jobs
def initialize(config, path = nil) def initialize(config, path = nil)
@ci_config = Gitlab::Ci::Config.new(config) @ci_config = Gitlab::Ci::Config.new(config)
......
...@@ -53,6 +53,10 @@ module Gitlab ...@@ -53,6 +53,10 @@ module Gitlab
} }
end end
def sym_options_with_owner
sym_options.merge(owner: OWNER)
end
def protection_options def protection_options
{ {
"Not protected: Both developers and masters can push new commits, force push, or delete the branch." => PROTECTION_NONE, "Not protected: Both developers and masters can push new commits, force push, or delete the branch." => PROTECTION_NONE,
......
...@@ -20,13 +20,8 @@ module Gitlab ...@@ -20,13 +20,8 @@ module Gitlab
def token def token
Gitlab::Redis.with do |redis| Gitlab::Redis.with do |redis|
token = redis.get(redis_key) token = redis.get(redis_key)
token ||= Devise.friendly_token(TOKEN_LENGTH)
if token redis.set(redis_key, token, ex: EXPIRY_TIME)
redis.expire(redis_key, EXPIRY_TIME)
else
token = Devise.friendly_token(TOKEN_LENGTH)
redis.set(redis_key, token, ex: EXPIRY_TIME)
end
token token
end end
......
...@@ -11,13 +11,6 @@ module Gitlab ...@@ -11,13 +11,6 @@ module Gitlab
DEFAULT_REDIS_URL = 'redis://localhost:6379' DEFAULT_REDIS_URL = 'redis://localhost:6379'
CONFIG_FILE = File.expand_path('../../config/resque.yml', __dir__) CONFIG_FILE = File.expand_path('../../config/resque.yml', __dir__)
# To be thread-safe we must be careful when writing the class instance
# variables @_raw_config and @pool. Because @pool depends on @_raw_config we need two
# mutexes to prevent deadlock.
RAW_CONFIG_MUTEX = Mutex.new
POOL_MUTEX = Mutex.new
private_constant :RAW_CONFIG_MUTEX, :POOL_MUTEX
class << self class << self
# Do NOT cache in an instance variable. Result may be mutated by caller. # Do NOT cache in an instance variable. Result may be mutated by caller.
def params def params
...@@ -31,24 +24,19 @@ module Gitlab ...@@ -31,24 +24,19 @@ module Gitlab
end end
def with def with
if @pool.nil? @pool ||= ConnectionPool.new { ::Redis.new(params) }
POOL_MUTEX.synchronize do
@pool = ConnectionPool.new { ::Redis.new(params) }
end
end
@pool.with { |redis| yield redis } @pool.with { |redis| yield redis }
end end
def _raw_config def _raw_config
return @_raw_config if defined?(@_raw_config) return @_raw_config if defined?(@_raw_config)
RAW_CONFIG_MUTEX.synchronize do begin
begin @_raw_config = File.read(CONFIG_FILE).freeze
@_raw_config = File.read(CONFIG_FILE).freeze rescue Errno::ENOENT
rescue Errno::ENOENT @_raw_config = false
@_raw_config = false
end
end end
@_raw_config @_raw_config
end end
end end
......
...@@ -16,21 +16,6 @@ retry() { ...@@ -16,21 +16,6 @@ retry() {
} }
if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then
mkdir -p vendor/apt
# Install phantomjs package
pushd vendor/apt
PHANTOMJS_FILE="phantomjs-$PHANTOMJS_VERSION-linux-x86_64"
if [ ! -d "$PHANTOMJS_FILE" ]; then
curl -q -L "https://s3.amazonaws.com/gitlab-build-helpers/$PHANTOMJS_FILE.tar.bz2" | tar jx
fi
cp "$PHANTOMJS_FILE/bin/phantomjs" "/usr/bin/"
popd
# Try to install packages
retry 'apt-get update -yqqq; apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \
libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip'
cp config/database.yml.mysql config/database.yml cp config/database.yml.mysql config/database.yml
sed -i 's/username:.*/username: root/g' config/database.yml sed -i 's/username:.*/username: root/g' config/database.yml
sed -i 's/password:.*/password:/g' config/database.yml sed -i 's/password:.*/password:/g' config/database.yml
......
...@@ -644,6 +644,20 @@ describe Projects::MergeRequestsController do ...@@ -644,6 +644,20 @@ describe Projects::MergeRequestsController do
end end
end end
context 'POST remove_wip' do
it 'removes the wip status' do
merge_request.title = merge_request.wip_title
merge_request.save
post :remove_wip,
namespace_id: merge_request.project.namespace.to_param,
project_id: merge_request.project.to_param,
id: merge_request.iid
expect(merge_request.reload.title).to eq(merge_request.wipless_title)
end
end
context 'POST resolve_conflicts' do context 'POST resolve_conflicts' do
let(:json_response) { JSON.parse(response.body) } let(:json_response) { JSON.parse(response.body) }
let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha } let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha }
......
...@@ -13,7 +13,7 @@ describe Projects::TemplatesController do ...@@ -13,7 +13,7 @@ describe Projects::TemplatesController do
end end
before do before do
project.team.add_user(user, Gitlab::Access::MASTER) project.add_user(user, Gitlab::Access::MASTER)
project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false) project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
end end
......
...@@ -4,24 +4,9 @@ FactoryGirl.define do ...@@ -4,24 +4,9 @@ FactoryGirl.define do
project project
master master
trait :guest do trait(:guest) { access_level ProjectMember::GUEST }
access_level ProjectMember::GUEST trait(:reporter) { access_level ProjectMember::REPORTER }
end trait(:developer) { access_level ProjectMember::DEVELOPER }
trait(:master) { access_level ProjectMember::MASTER }
trait :reporter do
access_level ProjectMember::REPORTER
end
trait :developer do
access_level ProjectMember::DEVELOPER
end
trait :master do
access_level ProjectMember::MASTER
end
trait :owner do
access_level ProjectMember::OWNER
end
end end
end end
...@@ -5,7 +5,9 @@ feature 'Contributions Calendar', js: true, feature: true do ...@@ -5,7 +5,9 @@ feature 'Contributions Calendar', js: true, feature: true do
let(:contributed_project) { create(:project, :public) } let(:contributed_project) { create(:project, :public) }
date_format = '%A %b %d, %Y' # Ex/ Sunday Jan 1, 2016
date_format = '%A %b %-d, %Y'
issue_title = 'Bug in old browser' issue_title = 'Bug in old browser'
issue_params = { title: issue_title } issue_params = { title: issue_title }
......
...@@ -12,15 +12,16 @@ describe "Compare", js: true do ...@@ -12,15 +12,16 @@ describe "Compare", js: true do
describe "branches" do describe "branches" do
it "pre-populates fields" do it "pre-populates fields" do
expect(page.find_field("from").value).to eq("master") expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("master")
expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("master")
end end
it "compares branches" do it "compares branches" do
fill_in "from", with: "fea" select_using_dropdown "from", "feature"
find("#from").click expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("feature")
click_link "feature" select_using_dropdown "to", "binary-encoding"
expect(page.find_field("from").value).to eq("feature") expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("binary-encoding")
click_button "Compare" click_button "Compare"
expect(page).to have_content "Commits" expect(page).to have_content "Commits"
...@@ -29,14 +30,21 @@ describe "Compare", js: true do ...@@ -29,14 +30,21 @@ describe "Compare", js: true do
describe "tags" do describe "tags" do
it "compares tags" do it "compares tags" do
fill_in "from", with: "v1.0" select_using_dropdown "from", "v1.0.0"
find("#from").click expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("v1.0.0")
click_link "v1.0.0" select_using_dropdown "to", "v1.1.0"
expect(page.find_field("from").value).to eq("v1.0.0") expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("v1.1.0")
click_button "Compare" click_button "Compare"
expect(page).to have_content "Commits" expect(page).to have_content "Commits"
end end
end end
def select_using_dropdown(dropdown_type, selection)
dropdown = find(".js-compare-#{dropdown_type}-dropdown")
dropdown.find(".compare-dropdown-toggle").click
dropdown.fill_in("Filter by branch/tag", with: selection)
click_link selection
end
end end
...@@ -21,6 +21,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do ...@@ -21,6 +21,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
click_link 'No Milestone' click_link 'No Milestone'
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_selector('.issue', count: 1) expect(page).to have_selector('.issue', count: 1)
end end
...@@ -29,6 +30,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do ...@@ -29,6 +30,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
click_link 'Any Milestone' click_link 'Any Milestone'
expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
expect(page).to have_selector('.issue', count: 2) expect(page).to have_selector('.issue', count: 2)
end end
...@@ -39,6 +41,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do ...@@ -39,6 +41,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
click_link milestone.title click_link milestone.title
end end
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_selector('.issue', count: 1) expect(page).to have_selector('.issue', count: 1)
end end
end end
......
...@@ -68,7 +68,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do ...@@ -68,7 +68,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 'expanding a diff for a renamed file' do context 'expanding a diff for a renamed file' do
before do before do
large_diff_renamed.find('.nothing-here-block').click large_diff_renamed.find('.click-to-expand').click
wait_for_ajax wait_for_ajax
end end
...@@ -87,7 +87,10 @@ feature 'Expand and collapse diffs', js: true, feature: true do ...@@ -87,7 +87,10 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 'expanding a large diff' do context 'expanding a large diff' do
before do before do
click_link('large_diff.md') # Wait for diffs
find('.file-title', match: :first)
# Click `large_diff.md` title
all('.file-title')[1].click
wait_for_ajax wait_for_ajax
end end
...@@ -128,7 +131,10 @@ feature 'Expand and collapse diffs', js: true, feature: true do ...@@ -128,7 +131,10 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 'expanding the diff' do context 'expanding the diff' do
before do before do
click_link('large_diff.md') # Wait for diffs
find('.file-title', match: :first)
# Click `large_diff.md` title
all('.file-title')[1].click
wait_for_ajax wait_for_ajax
end end
...@@ -146,7 +152,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do ...@@ -146,7 +152,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do
end end
context 'collapsing an expanded diff' do context 'collapsing an expanded diff' do
before { click_link('small_diff.md') } before do
# Wait for diffs
find('.file-title', match: :first)
# Click `small_diff.md` title
all('.file-title')[3].click
end
it 'hides the diff content' do it 'hides the diff content' do
expect(small_diff).not_to have_selector('.code') expect(small_diff).not_to have_selector('.code')
...@@ -154,7 +165,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do ...@@ -154,7 +165,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do
end end
context 're-expanding the same diff' do context 're-expanding the same diff' do
before { click_link('small_diff.md') } before do
# Wait for diffs
find('.file-title', match: :first)
# Click `small_diff.md` title
all('.file-title')[3].click
end
it 'shows the diff content' do it 'shows the diff content' do
expect(small_diff).to have_selector('.code') expect(small_diff).to have_selector('.code')
...@@ -231,7 +247,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do ...@@ -231,7 +247,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do
end end
context 'collapsing an expanded diff' do context 'collapsing an expanded diff' do
before { click_link('small_diff.md') } before do
# Wait for diffs
find('.file-title', match: :first)
# Click `small_diff.md` title
all('.file-title')[3].click
end
it 'hides the diff content' do it 'hides the diff content' do
expect(small_diff).not_to have_selector('.code') expect(small_diff).not_to have_selector('.code')
...@@ -239,7 +260,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do ...@@ -239,7 +260,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do
end end
context 're-expanding the same diff' do context 're-expanding the same diff' do
before { click_link('small_diff.md') } before do
# Wait for diffs
find('.file-title', match: :first)
# Click `small_diff.md` title
all('.file-title')[3].click
end
it 'shows the diff content' do it 'shows the diff content' do
expect(small_diff).to have_selector('.code') expect(small_diff).to have_selector('.code')
......
require 'rails_helper' require 'rails_helper'
feature 'Issue filtering by Labels', feature: true do feature 'Issue filtering by Labels', feature: true, js: true do
include WaitForAjax include WaitForAjax
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
let!(:user) { create(:user)} let!(:user) { create(:user) }
let!(:label) { create(:label, project: project) } let!(:label) { create(:label, project: project) }
before do before do
...@@ -28,156 +28,81 @@ feature 'Issue filtering by Labels', feature: true do ...@@ -28,156 +28,81 @@ feature 'Issue filtering by Labels', feature: true do
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project.namespace, project)
end end
context 'filter by label bug', js: true do context 'filter by label bug' do
before do before do
page.find('.js-label-select').click select_labels('bug')
wait_for_ajax
execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
wait_for_ajax
end end
it 'shows issue "Bugfix1" and "Bugfix2" in issues list' do it 'apply the filter' do
expect(page).to have_content "Bugfix1" expect(page).to have_content "Bugfix1"
expect(page).to have_content "Bugfix2" expect(page).to have_content "Bugfix2"
end
it 'does not show "Feature1" in issues list' do
expect(page).not_to have_content "Feature1" expect(page).not_to have_content "Feature1"
end
it 'shows label "bug" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "bug" expect(find('.filtered-labels')).to have_content "bug"
end
it 'does not show label "feature" and "enhancement" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "feature" expect(find('.filtered-labels')).not_to have_content "feature"
expect(find('.filtered-labels')).not_to have_content "enhancement" expect(find('.filtered-labels')).not_to have_content "enhancement"
end
it 'removes label "bug"' do
find('.js-label-filter-remove').click find('.js-label-filter-remove').click
wait_for_ajax wait_for_ajax
expect(find('.filtered-labels', visible: false)).to have_no_content "bug" expect(find('.filtered-labels', visible: false)).to have_no_content "bug"
end end
end end
context 'filter by label feature', js: true do context 'filter by label feature' do
before do before do
page.find('.js-label-select').click select_labels('feature')
wait_for_ajax
execute_script("$('.dropdown-menu-labels li:contains(\"feature\") a').click()")
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
wait_for_ajax
end end
it 'shows issue "Feature1" in issues list' do it 'applies the filter' do
expect(page).to have_content "Feature1" expect(page).to have_content "Feature1"
end
it 'does not show "Bugfix1" and "Bugfix2" in issues list' do
expect(page).not_to have_content "Bugfix2" expect(page).not_to have_content "Bugfix2"
expect(page).not_to have_content "Bugfix1" expect(page).not_to have_content "Bugfix1"
end
it 'shows label "feature" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "feature" expect(find('.filtered-labels')).to have_content "feature"
end
it 'does not show label "bug" and "enhancement" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "bug" expect(find('.filtered-labels')).not_to have_content "bug"
expect(find('.filtered-labels')).not_to have_content "enhancement" expect(find('.filtered-labels')).not_to have_content "enhancement"
end end
end end
context 'filter by label enhancement', js: true do context 'filter by label enhancement' do
before do before do
page.find('.js-label-select').click select_labels('enhancement')
wait_for_ajax
execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()")
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
wait_for_ajax
end end
it 'shows issue "Bugfix2" in issues list' do it 'applies the filter' do
expect(page).to have_content "Bugfix2" expect(page).to have_content "Bugfix2"
end
it 'does not show "Feature1" and "Bugfix1" in issues list' do
expect(page).not_to have_content "Feature1" expect(page).not_to have_content "Feature1"
expect(page).not_to have_content "Bugfix1" expect(page).not_to have_content "Bugfix1"
end
it 'shows label "enhancement" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "enhancement" expect(find('.filtered-labels')).to have_content "enhancement"
end
it 'does not show label "feature" and "bug" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "bug" expect(find('.filtered-labels')).not_to have_content "bug"
expect(find('.filtered-labels')).not_to have_content "feature" expect(find('.filtered-labels')).not_to have_content "feature"
end end
end end
context 'filter by label enhancement or feature', js: true do context 'filter by label enhancement and bug in issues list' do
before do before do
page.find('.js-label-select').click select_labels('bug', 'enhancement')
wait_for_ajax
execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()")
execute_script("$('.dropdown-menu-labels li:contains(\"feature\") a').click()")
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
wait_for_ajax
end end
it 'does not show "Bugfix1" or "Feature1" in issues list' do it 'applies the filters' do
expect(page).not_to have_content "Bugfix1" expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_content "Bugfix2"
expect(page).not_to have_content "Feature1" expect(page).not_to have_content "Feature1"
end expect(find('.filtered-labels')).to have_content "bug"
it 'shows label "enhancement" and "feature" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "enhancement" expect(find('.filtered-labels')).to have_content "enhancement"
expect(find('.filtered-labels')).to have_content "feature" expect(find('.filtered-labels')).not_to have_content "feature"
end
it 'does not show label "bug" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "bug"
end
it 'removes label "enhancement"' do
find('.js-label-filter-remove', match: :first).click find('.js-label-filter-remove', match: :first).click
wait_for_ajax wait_for_ajax
expect(find('.filtered-labels')).to have_no_content "enhancement"
end
end
context 'filter by label enhancement and bug in issues list', js: true do
before do
page.find('.js-label-select').click
wait_for_ajax
execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()")
execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
wait_for_ajax
end
it 'shows issue "Bugfix2" in issues list' do
expect(page).to have_content "Bugfix2" expect(page).to have_content "Bugfix2"
end
it 'does not show "Feature1"' do
expect(page).not_to have_content "Feature1" expect(page).not_to have_content "Feature1"
end expect(page).not_to have_content "Bugfix1"
expect(find('.filtered-labels')).not_to have_content "bug"
it 'shows label "bug" and "enhancement" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "bug"
expect(find('.filtered-labels')).to have_content "enhancement" expect(find('.filtered-labels')).to have_content "enhancement"
end
it 'does not show label "feature" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "feature" expect(find('.filtered-labels')).not_to have_content "feature"
end end
end end
context 'remove filtered labels', js: true do context 'remove filtered labels' do
before do before do
page.within '.labels-filter' do page.within '.labels-filter' do
click_button 'Label' click_button 'Label'
...@@ -200,7 +125,7 @@ feature 'Issue filtering by Labels', feature: true do ...@@ -200,7 +125,7 @@ feature 'Issue filtering by Labels', feature: true do
end end
end end
context 'dropdown filtering', js: true do context 'dropdown filtering' do
it 'filters by label name' do it 'filters by label name' do
page.within '.labels-filter' do page.within '.labels-filter' do
click_button 'Label' click_button 'Label'
...@@ -214,4 +139,14 @@ feature 'Issue filtering by Labels', feature: true do ...@@ -214,4 +139,14 @@ feature 'Issue filtering by Labels', feature: true do
end end
end end
end end
def select_labels(*labels)
page.find('.js-label-select').click
wait_for_ajax
labels.each do |label|
execute_script("$('.dropdown-menu-labels li:contains(\"#{label}\") a').click()")
end
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
wait_for_ajax
end
end end
...@@ -7,15 +7,15 @@ describe 'Filter issues', feature: true do ...@@ -7,15 +7,15 @@ describe 'Filter issues', feature: true do
let!(:user) { create(:user)} let!(:user) { create(:user)}
let!(:milestone) { create(:milestone, project: project) } let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) } let!(:label) { create(:label, project: project) }
let!(:issue1) { create(:issue, project: project) }
let!(:wontfix) { create(:label, project: project, title: "Won't fix") } let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
before do before do
project.team << [user, :master] project.team << [user, :master]
login_as(user) login_as(user)
create(:issue, project: project)
end end
describe 'Filter issues for assignee from issues#index' do describe 'for assignee from issues#index' do
before do before do
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project.namespace, project)
...@@ -45,7 +45,7 @@ describe 'Filter issues', feature: true do ...@@ -45,7 +45,7 @@ describe 'Filter issues', feature: true do
end end
end end
describe 'Filter issues for milestone from issues#index' do describe 'for milestone from issues#index' do
before do before do
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project.namespace, project)
...@@ -75,7 +75,7 @@ describe 'Filter issues', feature: true do ...@@ -75,7 +75,7 @@ describe 'Filter issues', feature: true do
end end
end end
describe 'Filter issues for label from issues#index', js: true do describe 'for label from issues#index', js: true do
before do before do
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project.namespace, project)
find('.js-label-select').click find('.js-label-select').click
...@@ -115,6 +115,7 @@ describe 'Filter issues', feature: true do ...@@ -115,6 +115,7 @@ describe 'Filter issues', feature: true do
expect(page).to have_content wontfix.title expect(page).to have_content wontfix.title
click_link wontfix.title click_link wontfix.title
end end
expect(find('.js-label-select .dropdown-toggle-text')).to have_content(wontfix.title) expect(find('.js-label-select .dropdown-toggle-text')).to have_content(wontfix.title)
end end
...@@ -146,7 +147,7 @@ describe 'Filter issues', feature: true do ...@@ -146,7 +147,7 @@ describe 'Filter issues', feature: true do
end end
end end
describe 'Filter issues for assignee and label from issues#index' do describe 'for assignee and label from issues#index' do
before do before do
visit namespace_project_issues_path(project.namespace, project) visit namespace_project_issues_path(project.namespace, project)
...@@ -226,6 +227,7 @@ describe 'Filter issues', feature: true do ...@@ -226,6 +227,7 @@ describe 'Filter issues', feature: true do
it 'filters by text and label' do it 'filters by text and label' do
fill_in 'issuable_search', with: 'Bug' fill_in 'issuable_search', with: 'Bug'
expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
page.within '.issues-list' do page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2) expect(page).to have_selector('.issue', count: 2)
end end
...@@ -236,6 +238,7 @@ describe 'Filter issues', feature: true do ...@@ -236,6 +238,7 @@ describe 'Filter issues', feature: true do
end end
find('.dropdown-menu-close-icon').click find('.dropdown-menu-close-icon').click
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
page.within '.issues-list' do page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 1) expect(page).to have_selector('.issue', count: 1)
end end
...@@ -244,6 +247,7 @@ describe 'Filter issues', feature: true do ...@@ -244,6 +247,7 @@ describe 'Filter issues', feature: true do
it 'filters by text and milestone' do it 'filters by text and milestone' do
fill_in 'issuable_search', with: 'Bug' fill_in 'issuable_search', with: 'Bug'
expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
page.within '.issues-list' do page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2) expect(page).to have_selector('.issue', count: 2)
end end
...@@ -253,6 +257,7 @@ describe 'Filter issues', feature: true do ...@@ -253,6 +257,7 @@ describe 'Filter issues', feature: true do
click_link '8' click_link '8'
end end
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
page.within '.issues-list' do page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 1) expect(page).to have_selector('.issue', count: 1)
end end
...@@ -261,6 +266,7 @@ describe 'Filter issues', feature: true do ...@@ -261,6 +266,7 @@ describe 'Filter issues', feature: true do
it 'filters by text and assignee' do it 'filters by text and assignee' do
fill_in 'issuable_search', with: 'Bug' fill_in 'issuable_search', with: 'Bug'
expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
page.within '.issues-list' do page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2) expect(page).to have_selector('.issue', count: 2)
end end
...@@ -270,6 +276,7 @@ describe 'Filter issues', feature: true do ...@@ -270,6 +276,7 @@ describe 'Filter issues', feature: true do
click_link user.name click_link user.name
end end
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
page.within '.issues-list' do page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 1) expect(page).to have_selector('.issue', count: 1)
end end
...@@ -278,6 +285,7 @@ describe 'Filter issues', feature: true do ...@@ -278,6 +285,7 @@ describe 'Filter issues', feature: true do
it 'filters by text and author' do it 'filters by text and author' do
fill_in 'issuable_search', with: 'Bug' fill_in 'issuable_search', with: 'Bug'
expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
page.within '.issues-list' do page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2) expect(page).to have_selector('.issue', count: 2)
end end
...@@ -287,6 +295,7 @@ describe 'Filter issues', feature: true do ...@@ -287,6 +295,7 @@ describe 'Filter issues', feature: true do
click_link user.name click_link user.name
end end
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
page.within '.issues-list' do page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 1) expect(page).to have_selector('.issue', count: 1)
end end
...@@ -315,6 +324,7 @@ describe 'Filter issues', feature: true do ...@@ -315,6 +324,7 @@ describe 'Filter issues', feature: true do
find('.dropdown-menu-close-icon').click find('.dropdown-menu-close-icon').click
wait_for_ajax wait_for_ajax
expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
page.within '.issues-list' do page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2) expect(page).to have_selector('.issue', count: 2)
end end
......
...@@ -99,5 +99,15 @@ feature 'Issues > User uses slash commands', feature: true, js: true do ...@@ -99,5 +99,15 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
end end
end end
end end
describe 'toggling the WIP prefix from the title from note' do
let(:issue) { create(:issue, project: project) }
it 'does not recognize the command nor create a note' do
write_note("/wip")
expect(page).not_to have_content '/wip'
end
end
end end
end end
...@@ -17,6 +17,7 @@ feature 'Merge Request filtering by Milestone', feature: true do ...@@ -17,6 +17,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
visit_merge_requests(project) visit_merge_requests(project)
filter_by_milestone(Milestone::None.title) filter_by_milestone(Milestone::None.title)
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1) expect(page).to have_css('.merge-request', count: 1)
end end
...@@ -39,6 +40,7 @@ feature 'Merge Request filtering by Milestone', feature: true do ...@@ -39,6 +40,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
visit_merge_requests(project) visit_merge_requests(project)
filter_by_milestone(Milestone::Upcoming.title) filter_by_milestone(Milestone::Upcoming.title)
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1) expect(page).to have_css('.merge-request', count: 1)
end end
...@@ -61,6 +63,7 @@ feature 'Merge Request filtering by Milestone', feature: true do ...@@ -61,6 +63,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
visit_merge_requests(project) visit_merge_requests(project)
filter_by_milestone(milestone.title) filter_by_milestone(milestone.title)
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1) expect(page).to have_css('.merge-request', count: 1)
end end
......
require 'spec_helper' require 'spec_helper'
feature 'Merge Request versions', js: true, feature: true do feature 'Merge Request versions', js: true, feature: true do
let(:merge_request) { create(:merge_request, importing: true) }
let(:project) { merge_request.source_project }
before do before do
login_as :admin login_as :admin
merge_request = create(:merge_request, importing: true)
merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
project = merge_request.source_project
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
end end
...@@ -47,6 +48,16 @@ feature 'Merge Request versions', js: true, feature: true do ...@@ -47,6 +48,16 @@ feature 'Merge Request versions', js: true, feature: true do
end end
end end
it 'has a path with comparison context' do
expect(page).to have_current_path diffs_namespace_project_merge_request_path(
project.namespace,
project,
merge_request.iid,
diff_id: 2,
start_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
)
end
it 'should have correct value in the compare dropdown' do it 'should have correct value in the compare dropdown' do
page.within '.mr-version-compare-dropdown' do page.within '.mr-version-compare-dropdown' do
expect(page).to have_content 'version 1' expect(page).to have_content 'version 1'
...@@ -61,10 +72,6 @@ feature 'Merge Request versions', js: true, feature: true do ...@@ -61,10 +72,6 @@ feature 'Merge Request versions', js: true, feature: true do
expect(page).to have_content '4 changed files with 15 additions and 6 deletions' expect(page).to have_content '4 changed files with 15 additions and 6 deletions'
end end
it 'show diff between new and old version' do
expect(page).to have_content '4 changed files with 15 additions and 6 deletions'
end
it 'should return to latest version when "Show latest version" button is clicked' do it 'should return to latest version when "Show latest version" button is clicked' do
click_link 'Show latest version' click_link 'Show latest version'
page.within '.mr-version-dropdown' do page.within '.mr-version-dropdown' do
......
...@@ -14,21 +14,66 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do ...@@ -14,21 +14,66 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } } let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
end end
describe 'adding a due date from note' do describe 'merge-request-only commands' do
before do before do
project.team << [user, :master] project.team << [user, :master]
login_with(user) login_with(user)
visit namespace_project_merge_request_path(project.namespace, project, merge_request) visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end end
after do after do
wait_for_ajax wait_for_ajax
end end
it 'does not recognize the command nor create a note' do describe 'toggling the WIP prefix in the title from note' do
write_note("/due 2016-08-28") context 'when the current user can toggle the WIP prefix' do
it 'adds the WIP: prefix to the title' do
write_note("/wip")
expect(page).not_to have_content '/wip'
expect(page).to have_content 'Your commands have been executed!'
expect(merge_request.reload.work_in_progress?).to eq true
end
it 'removes the WIP: prefix from the title' do
merge_request.title = merge_request.wip_title
merge_request.save
write_note("/wip")
expect(page).not_to have_content '/wip'
expect(page).to have_content 'Your commands have been executed!'
expect(merge_request.reload.work_in_progress?).to eq false
end
end
context 'when the current user cannot toggle the WIP prefix' do
let(:guest) { create(:user) }
before do
project.team << [guest, :guest]
logout
login_with(guest)
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
it 'does not change the WIP prefix' do
write_note("/wip")
expect(page).not_to have_content '/wip'
expect(page).not_to have_content 'Your commands have been executed!'
expect(merge_request.reload.work_in_progress?).to eq false
end
end
end
describe 'adding a due date from note' do
it 'does not recognize the command nor create a note' do
write_note('/due 2016-08-28')
expect(page).not_to have_content '/due 2016-08-28' expect(page).not_to have_content '/due 2016-08-28'
end
end end
end end
end end
require 'rails_helper' require 'rails_helper'
describe 'Profile > SSH Keys', feature: true do feature 'Profile > SSH Keys', feature: true do
let(:user) { create(:user) } let(:user) { create(:user) }
before do before do
login_as(user) login_as(user)
visit profile_keys_path
end end
describe 'User adds an SSH key' do describe 'User adds a key' do
it 'auto-populates the title', js: true do before do
visit profile_keys_path
end
scenario 'auto-populates the title', js: true do
fill_in('Key', with: attributes_for(:key).fetch(:key)) fill_in('Key', with: attributes_for(:key).fetch(:key))
expect(find_field('Title').value).to eq 'dummy@gitlab.com' expect(find_field('Title').value).to eq 'dummy@gitlab.com'
end end
scenario 'saves the new key' do
attrs = attributes_for(:key)
fill_in('Key', with: attrs[:key])
fill_in('Title', with: attrs[:title])
click_button('Add key')
expect(page).to have_content("Title: #{attrs[:title]}")
expect(page).to have_content(attrs[:key])
end
end
scenario 'User sees their keys' do
key = create(:key, user: user)
visit profile_keys_path
expect(page).to have_content(key.title)
end
scenario 'User removes a key via the key index' do
create(:key, user: user)
visit profile_keys_path
click_link('Remove')
expect(page).to have_content('Your SSH keys (0)')
end
scenario 'User removes a key via its details page' do
key = create(:key, user: user)
visit profile_key_path(key)
click_link('Remove')
expect(page).to have_content('Your SSH keys (0)')
end end
end end
require 'spec_helper'
feature 'User uses soft wrap whilst editing file', feature: true, js: true do
before do
user = create(:user)
project = create(:project)
project.team << [user, :master]
login_as user
visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: 'test_file-name')
editor = find('.file-editor.code')
editor.click
editor.send_keys 'Touch water with paw then recoil in horror chase dog then
run away chase the pig around the house eat owner\'s food, and knock
dish off table head butt cant eat out of my own dish. Cat is love, cat
is life rub face on everything poop on grasses so meow. Playing with
balls of wool flee in terror at cucumber discovered on floor run in
circles tuxedo cats always looking dapper, but attack dog, run away
and pretend to be victim so all of a sudden cat goes crazy, yet chase
laser. Make muffins sit in window and stare ooo, a bird! yum lick yarn
hanging out of own butt jump off balcony, onto stranger\'s head yet
chase laser. Purr for no reason stare at ceiling hola te quiero.'.squish
end
let(:toggle_button) { find('.soft-wrap-toggle') }
scenario 'user clicks the "Soft wrap" button and then "No wrap" button' do
wrapped_content_width = get_content_width
toggle_button.click
expect(toggle_button).to have_content 'No wrap'
unwrapped_content_width = get_content_width
expect(unwrapped_content_width).to be < wrapped_content_width
toggle_button.click
expect(toggle_button).to have_content 'Soft wrap'
expect(get_content_width).to be > unwrapped_content_width
end
def get_content_width
find('.ace_content')[:style].slice!(/width: \d+/).slice!(/\d+/)
end
end
...@@ -86,14 +86,14 @@ feature 'Import/Export - project import integration test', feature: true, js: tr ...@@ -86,14 +86,14 @@ feature 'Import/Export - project import integration test', feature: true, js: tr
login_as(normal_user) login_as(normal_user)
end end
scenario 'non-admin user is not allowed to import a project' do scenario 'non-admin user is allowed to import a project' do
expect(Project.all.count).to be_zero expect(Project.all.count).to be_zero
visit new_project_path visit new_project_path
fill_in :project_path, with: 'test-project-path', visible: true fill_in :project_path, with: 'test-project-path', visible: true
expect(page).not_to have_content('GitLab export') expect(page).to have_content('GitLab export')
end end
end end
......
require 'spec_helper' require 'spec_helper'
feature 'Projects > Members > Owner cannot leave project', feature: true do feature 'Projects > Members > Owner cannot leave project', feature: true do
let(:owner) { create(:user) }
let(:project) { create(:project) } let(:project) { create(:project) }
background do background do
project.team << [owner, :owner] login_as(project.owner)
login_as(owner)
visit namespace_project_path(project.namespace, project) visit namespace_project_path(project.namespace, project)
end end
......
require 'spec_helper' require 'spec_helper'
feature 'Projects > Members > Owner cannot request access to his project', feature: true do feature 'Projects > Members > Owner cannot request access to his project', feature: true do
let(:owner) { create(:user) }
let(:project) { create(:project) } let(:project) { create(:project) }
background do background do
project.team << [owner, :owner] login_as(project.owner)
login_as(owner)
visit namespace_project_path(project.namespace, project) visit namespace_project_path(project.namespace, project)
end end
......
...@@ -82,7 +82,7 @@ feature 'Project', feature: true do ...@@ -82,7 +82,7 @@ feature 'Project', feature: true do
before do before do
login_with(user) login_with(user)
project.team.add_user(user, Gitlab::Access::MASTER) project.add_user(user, Gitlab::Access::MASTER)
visit namespace_project_path(project.namespace, project) visit namespace_project_path(project.namespace, project)
end end
...@@ -101,8 +101,8 @@ feature 'Project', feature: true do ...@@ -101,8 +101,8 @@ feature 'Project', feature: true do
context 'on issues page', js: true do context 'on issues page', js: true do
before do before do
login_with(user) login_with(user)
project.team.add_user(user, Gitlab::Access::MASTER) project.add_user(user, Gitlab::Access::MASTER)
project2.team.add_user(user, Gitlab::Access::MASTER) project2.add_user(user, Gitlab::Access::MASTER)
visit namespace_project_issue_path(project.namespace, project, issue) visit namespace_project_issue_path(project.namespace, project, issue)
end end
......
require 'spec_helper'
describe AccessRequestsFinder, services: true do
let(:user) { create(:user) }
let(:access_requester) { create(:user) }
let(:project) { create(:project, :public) }
let(:group) { create(:group, :public) }
before do
project.request_access(access_requester)
group.request_access(access_requester)
end
shared_examples 'a finder returning access requesters' do |method_name|
it 'returns access requesters' do
access_requesters = described_class.new(source).public_send(method_name, user)
expect(access_requesters.size).to eq(1)
expect(access_requesters.first).to be_a "#{source.class.to_s}Member".constantize
expect(access_requesters.first.user).to eq(access_requester)
end
end
shared_examples 'a finder returning no results' do |method_name|
it 'raises Gitlab::Access::AccessDeniedError' do
expect(described_class.new(source).public_send(method_name, user)).to be_empty
end
end
shared_examples 'a finder raising Gitlab::Access::AccessDeniedError' do |method_name|
it 'raises Gitlab::Access::AccessDeniedError' do
expect { described_class.new(source).public_send(method_name, user) }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
describe '#execute' do
context 'when current user cannot see project access requests' do
it_behaves_like 'a finder returning no results', :execute do
let(:source) { project }
end
it_behaves_like 'a finder returning no results', :execute do
let(:source) { group }
end
end
context 'when current user can see access requests' do
before do
project.team << [user, :master]
group.add_owner(user)
end
it_behaves_like 'a finder returning access requesters', :execute do
let(:source) { project }
end
it_behaves_like 'a finder returning access requesters', :execute do
let(:source) { group }
end
end
end
describe '#execute!' do
context 'when current user cannot see access requests' do
it_behaves_like 'a finder raising Gitlab::Access::AccessDeniedError', :execute! do
let(:source) { project }
end
it_behaves_like 'a finder raising Gitlab::Access::AccessDeniedError', :execute! do
let(:source) { group }
end
end
context 'when current user can see access requests' do
before do
project.team << [user, :master]
group.add_owner(user)
end
it_behaves_like 'a finder returning access requesters', :execute! do
let(:source) { project }
end
it_behaves_like 'a finder returning access requesters', :execute! do
let(:source) { group }
end
end
end
end
...@@ -43,7 +43,7 @@ describe JoinedGroupsFinder do ...@@ -43,7 +43,7 @@ describe JoinedGroupsFinder do
context 'if profile visitor is in one of the private group projects' do context 'if profile visitor is in one of the private group projects' do
before do before do
project = create(:project, :private, group: private_group, name: 'B', path: 'B') project = create(:project, :private, group: private_group, name: 'B', path: 'B')
project.team.add_user(profile_visitor, Gitlab::Access::DEVELOPER) project.add_user(profile_visitor, Gitlab::Access::DEVELOPER)
end end
it 'shows group' do it 'shows group' do
......
...@@ -38,7 +38,7 @@ describe ProjectsFinder do ...@@ -38,7 +38,7 @@ describe ProjectsFinder do
describe 'with private projects' do describe 'with private projects' do
before do before do
private_project.team.add_user(user, Gitlab::Access::MASTER) private_project.add_user(user, Gitlab::Access::MASTER)
end end
it do it do
......
require 'spec_helper' require 'spec_helper'
describe IssuablesHelper do describe IssuablesHelper do
let(:label) { build_stubbed(:label) } let(:label) { build_stubbed(:label) }
let(:label2) { build_stubbed(:label) } let(:label2) { build_stubbed(:label) }
context 'label tooltip' do describe '#issuable_labels_tooltip' do
it 'returns label text' do it 'returns label text' do
expect(issuable_labels_tooltip([label])).to eq(label.title) expect(issuable_labels_tooltip([label])).to eq(label.title)
end end
...@@ -13,4 +13,105 @@ describe IssuablesHelper do ...@@ -13,4 +13,105 @@ describe IssuablesHelper do
expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more") expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more")
end end
end end
describe '#issuables_state_counter_text' do
let(:user) { create(:user) }
describe 'state text' do
before do
allow(helper).to receive(:issuables_count_for_state).and_return(42)
end
it 'returns "Open" when state is :opened' do
expect(helper.issuables_state_counter_text(:issues, :opened)).
to eq('<span>Open</span> <span class="badge">42</span>')
end
it 'returns "Closed" when state is :closed' do
expect(helper.issuables_state_counter_text(:issues, :closed)).
to eq('<span>Closed</span> <span class="badge">42</span>')
end
it 'returns "Merged" when state is :merged' do
expect(helper.issuables_state_counter_text(:merge_requests, :merged)).
to eq('<span>Merged</span> <span class="badge">42</span>')
end
it 'returns "All" when state is :all' do
expect(helper.issuables_state_counter_text(:merge_requests, :all)).
to eq('<span>All</span> <span class="badge">42</span>')
end
end
describe 'counter caching based on issuable type and params', :caching do
let(:params) do
{
scope: 'created-by-me',
state: 'opened',
utf8: '✓',
author_id: '11',
assignee_id: '18',
label_name: ['bug', 'discussion', 'documentation'],
milestone_title: 'v4.0',
sort: 'due_date_asc',
namespace_id: 'gitlab-org',
project_id: 'gitlab-ce',
page: 2
}.with_indifferent_access
end
it 'returns the cached value when called for the same issuable type & with the same params' do
expect(helper).to receive(:params).twice.and_return(params)
expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42)
expect(helper.issuables_state_counter_text(:issues, :opened)).
to eq('<span>Open</span> <span class="badge">42</span>')
expect(helper).not_to receive(:issuables_count_for_state)
expect(helper.issuables_state_counter_text(:issues, :opened)).
to eq('<span>Open</span> <span class="badge">42</span>')
end
it 'does not take some keys into account in the cache key' do
expect(helper).to receive(:params).and_return({
author_id: '11',
state: 'foo',
sort: 'foo',
utf8: 'foo',
page: 'foo'
}.with_indifferent_access)
expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42)
expect(helper.issuables_state_counter_text(:issues, :opened)).
to eq('<span>Open</span> <span class="badge">42</span>')
expect(helper).to receive(:params).and_return({
author_id: '11',
state: 'bar',
sort: 'bar',
utf8: 'bar',
page: 'bar'
}.with_indifferent_access)
expect(helper).not_to receive(:issuables_count_for_state)
expect(helper.issuables_state_counter_text(:issues, :opened)).
to eq('<span>Open</span> <span class="badge">42</span>')
end
it 'does not take params order into account in the cache key' do
expect(helper).to receive(:params).and_return('author_id' => '11', 'state' => 'opened')
expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42)
expect(helper.issuables_state_counter_text(:issues, :opened)).
to eq('<span>Open</span> <span class="badge">42</span>')
expect(helper).to receive(:params).and_return('state' => 'opened', 'author_id' => '11')
expect(helper).not_to receive(:issuables_count_for_state)
expect(helper.issuables_state_counter_text(:issues, :opened)).
to eq('<span>Open</span> <span class="badge">42</span>')
end
end
end
end end
...@@ -11,7 +11,7 @@ describe MembersHelper do ...@@ -11,7 +11,7 @@ describe MembersHelper do
describe '#remove_member_message' do describe '#remove_member_message' do
let(:requester) { build(:user) } let(:requester) { build(:user) }
let(:project) { create(:project) } let(:project) { create(:empty_project, :public) }
let(:project_member) { build(:project_member, project: project) } let(:project_member) { build(:project_member, project: project) }
let(:project_member_invite) { build(:project_member, project: project).tap { |m| m.generate_invite_token! } } let(:project_member_invite) { build(:project_member, project: project).tap { |m| m.generate_invite_token! } }
let(:project_member_request) { project.request_access(requester) } let(:project_member_request) { project.request_access(requester) }
...@@ -32,7 +32,7 @@ describe MembersHelper do ...@@ -32,7 +32,7 @@ describe MembersHelper do
describe '#remove_member_title' do describe '#remove_member_title' do
let(:requester) { build(:user) } let(:requester) { build(:user) }
let(:project) { create(:project) } let(:project) { create(:empty_project, :public) }
let(:project_member) { build(:project_member, project: project) } let(:project_member) { build(:project_member, project: project) }
let(:project_member_request) { project.request_access(requester) } let(:project_member_request) { project.request_access(requester) }
let(:group) { create(:group) } let(:group) { create(:group) }
......
...@@ -10,7 +10,7 @@ describe Gitlab::Template::IssueTemplate do ...@@ -10,7 +10,7 @@ describe Gitlab::Template::IssueTemplate do
let(:file_path_3) { '.gitlab/issue_templates/feature_proposal.md' } let(:file_path_3) { '.gitlab/issue_templates/feature_proposal.md' }
before do before do
project.team.add_user(user, Gitlab::Access::MASTER) project.add_user(user, Gitlab::Access::MASTER)
project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false) project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false) project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false) project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
...@@ -53,7 +53,7 @@ describe Gitlab::Template::IssueTemplate do ...@@ -53,7 +53,7 @@ describe Gitlab::Template::IssueTemplate do
context 'when repo is bare or empty' do context 'when repo is bare or empty' do
let(:empty_project) { create(:empty_project) } let(:empty_project) { create(:empty_project) }
before { empty_project.team.add_user(user, Gitlab::Access::MASTER) } before { empty_project.add_user(user, Gitlab::Access::MASTER) }
it "returns empty array" do it "returns empty array" do
templates = subject.by_category('', empty_project) templates = subject.by_category('', empty_project)
...@@ -78,7 +78,7 @@ describe Gitlab::Template::IssueTemplate do ...@@ -78,7 +78,7 @@ describe Gitlab::Template::IssueTemplate do
context "when repo is empty" do context "when repo is empty" do
let(:empty_project) { create(:empty_project) } let(:empty_project) { create(:empty_project) }
before { empty_project.team.add_user(user, Gitlab::Access::MASTER) } before { empty_project.add_user(user, Gitlab::Access::MASTER) }
it "raises file not found" do it "raises file not found" do
issue_template = subject.new('.gitlab/issue_templates/not_existent.md', empty_project) issue_template = subject.new('.gitlab/issue_templates/not_existent.md', empty_project)
......
...@@ -10,7 +10,7 @@ describe Gitlab::Template::MergeRequestTemplate do ...@@ -10,7 +10,7 @@ describe Gitlab::Template::MergeRequestTemplate do
let(:file_path_3) { '.gitlab/merge_request_templates/feature_proposal.md' } let(:file_path_3) { '.gitlab/merge_request_templates/feature_proposal.md' }
before do before do
project.team.add_user(user, Gitlab::Access::MASTER) project.add_user(user, Gitlab::Access::MASTER)
project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false) project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false) project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false) project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
...@@ -53,7 +53,7 @@ describe Gitlab::Template::MergeRequestTemplate do ...@@ -53,7 +53,7 @@ describe Gitlab::Template::MergeRequestTemplate do
context 'when repo is bare or empty' do context 'when repo is bare or empty' do
let(:empty_project) { create(:empty_project) } let(:empty_project) { create(:empty_project) }
before { empty_project.team.add_user(user, Gitlab::Access::MASTER) } before { empty_project.add_user(user, Gitlab::Access::MASTER) }
it "returns empty array" do it "returns empty array" do
templates = subject.by_category('', empty_project) templates = subject.by_category('', empty_project)
...@@ -78,7 +78,7 @@ describe Gitlab::Template::MergeRequestTemplate do ...@@ -78,7 +78,7 @@ describe Gitlab::Template::MergeRequestTemplate do
context "when repo is empty" do context "when repo is empty" do
let(:empty_project) { create(:empty_project) } let(:empty_project) { create(:empty_project) }
before { empty_project.team.add_user(user, Gitlab::Access::MASTER) } before { empty_project.add_user(user, Gitlab::Access::MASTER) }
it "raises file not found" do it "raises file not found" do
issue_template = subject.new('.gitlab/merge_request_templates/not_existent.md', empty_project) issue_template = subject.new('.gitlab/merge_request_templates/not_existent.md', empty_project)
......
...@@ -402,7 +402,7 @@ describe Notify do ...@@ -402,7 +402,7 @@ describe Notify do
describe 'project access requested' do describe 'project access requested' do
context 'for a project in a user namespace' do context 'for a project in a user namespace' do
let(:project) { create(:project).tap { |p| p.team << [p.owner, :master, p.owner] } } let(:project) { create(:project, :public).tap { |p| p.team << [p.owner, :master, p.owner] } }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project_member) do let(:project_member) do
project.request_access(user) project.request_access(user)
...@@ -429,7 +429,7 @@ describe Notify do ...@@ -429,7 +429,7 @@ describe Notify do
context 'for a project in a group' do context 'for a project in a group' do
let(:group_owner) { create(:user) } let(:group_owner) { create(:user) }
let(:group) { create(:group).tap { |g| g.add_owner(group_owner) } } let(:group) { create(:group).tap { |g| g.add_owner(group_owner) } }
let(:project) { create(:project, namespace: group) } let(:project) { create(:project, :public, namespace: group) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project_member) do let(:project_member) do
project.request_access(user) project.request_access(user)
...@@ -492,21 +492,22 @@ describe Notify do ...@@ -492,21 +492,22 @@ describe Notify do
end end
end end
def invite_to_project(project:, email:, inviter:) def invite_to_project(project, inviter:)
Member.add_user( create(
project.project_members, :project_member,
'toto@example.com', :developer,
Gitlab::Access::DEVELOPER, project: project,
current_user: inviter invite_token: '1234',
invite_email: 'toto@example.com',
user: nil,
created_by: inviter
) )
project.project_members.invite.last
end end
describe 'project invitation' do describe 'project invitation' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } } let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
let(:project_member) { invite_to_project(project: project, email: 'toto@example.com', inviter: master) } let(:project_member) { invite_to_project(project, inviter: master) }
subject { Notify.member_invited_email('project', project_member.id, project_member.invite_token) } subject { Notify.member_invited_email('project', project_member.id, project_member.invite_token) }
...@@ -525,10 +526,10 @@ describe Notify do ...@@ -525,10 +526,10 @@ describe Notify do
describe 'project invitation accepted' do describe 'project invitation accepted' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:invited_user) { create(:user) } let(:invited_user) { create(:user, name: 'invited user') }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } } let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
let(:project_member) do let(:project_member) do
invitee = invite_to_project(project: project, email: 'toto@example.com', inviter: master) invitee = invite_to_project(project, inviter: master)
invitee.accept_invite!(invited_user) invitee.accept_invite!(invited_user)
invitee invitee
end end
...@@ -552,7 +553,7 @@ describe Notify do ...@@ -552,7 +553,7 @@ describe Notify do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } } let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
let(:project_member) do let(:project_member) do
invitee = invite_to_project(project: project, email: 'toto@example.com', inviter: master) invitee = invite_to_project(project, inviter: master)
invitee.decline_invite! invitee.decline_invite!
invitee invitee
end end
...@@ -744,21 +745,22 @@ describe Notify do ...@@ -744,21 +745,22 @@ describe Notify do
end end
end end
def invite_to_group(group:, email:, inviter:) def invite_to_group(group, inviter:)
Member.add_user( create(
group.group_members, :group_member,
'toto@example.com', :developer,
Gitlab::Access::DEVELOPER, group: group,
current_user: inviter invite_token: '1234',
invite_email: 'toto@example.com',
user: nil,
created_by: inviter
) )
group.group_members.invite.last
end end
describe 'group invitation' do describe 'group invitation' do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } } let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
let(:group_member) { invite_to_group(group: group, email: 'toto@example.com', inviter: owner) } let(:group_member) { invite_to_group(group, inviter: owner) }
subject { Notify.member_invited_email('group', group_member.id, group_member.invite_token) } subject { Notify.member_invited_email('group', group_member.id, group_member.invite_token) }
...@@ -777,10 +779,10 @@ describe Notify do ...@@ -777,10 +779,10 @@ describe Notify do
describe 'group invitation accepted' do describe 'group invitation accepted' do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:invited_user) { create(:user) } let(:invited_user) { create(:user, name: 'invited user') }
let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } } let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
let(:group_member) do let(:group_member) do
invitee = invite_to_group(group: group, email: 'toto@example.com', inviter: owner) invitee = invite_to_group(group, inviter: owner)
invitee.accept_invite!(invited_user) invitee.accept_invite!(invited_user)
invitee invitee
end end
...@@ -804,7 +806,7 @@ describe Notify do ...@@ -804,7 +806,7 @@ describe Notify do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } } let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
let(:group_member) do let(:group_member) do
invitee = invite_to_group(group: group, email: 'toto@example.com', inviter: owner) invitee = invite_to_group(group, inviter: owner)
invitee.decline_invite! invitee.decline_invite!
invitee invitee
end end
......
...@@ -494,7 +494,7 @@ describe Issue, models: true do ...@@ -494,7 +494,7 @@ describe Issue, models: true do
context 'with an admin user' do context 'with an admin user' do
let(:project) { create(:empty_project) } let(:project) { create(:empty_project) }
let(:user) { create(:user, admin: true) } let(:user) { create(:admin) }
it 'returns true for a regular issue' do it 'returns true for a regular issue' do
issue = build(:issue, project: project) issue = build(:issue, project: project)
......
...@@ -57,7 +57,7 @@ describe Member, models: true do ...@@ -57,7 +57,7 @@ describe Member, models: true do
describe 'Scopes & finders' do describe 'Scopes & finders' do
before do before do
project = create(:empty_project) project = create(:empty_project, :public)
group = create(:group) group = create(:group)
@owner_user = create(:user).tap { |u| group.add_owner(u) } @owner_user = create(:user).tap { |u| group.add_owner(u) }
@owner = group.members.find_by(user_id: @owner_user.id) @owner = group.members.find_by(user_id: @owner_user.id)
...@@ -74,22 +74,17 @@ describe Member, models: true do ...@@ -74,22 +74,17 @@ describe Member, models: true do
@blocked_master = project.members.find_by(user_id: @blocked_user.id, access_level: Gitlab::Access::MASTER) @blocked_master = project.members.find_by(user_id: @blocked_user.id, access_level: Gitlab::Access::MASTER)
@blocked_developer = project.members.find_by(user_id: @blocked_user.id, access_level: Gitlab::Access::DEVELOPER) @blocked_developer = project.members.find_by(user_id: @blocked_user.id, access_level: Gitlab::Access::DEVELOPER)
Member.add_user( @invited_member = create(:project_member, :developer,
project.members, project: project,
'toto1@example.com', invite_token: '1234',
Gitlab::Access::DEVELOPER, invite_email: 'toto1@example.com')
current_user: @master_user
)
@invited_member = project.members.invite.find_by_invite_email('toto1@example.com')
accepted_invite_user = build(:user, state: :active) accepted_invite_user = build(:user, state: :active)
Member.add_user( @accepted_invite_member = create(:project_member, :developer,
project.members, project: project,
'toto2@example.com', invite_token: '1234',
Gitlab::Access::DEVELOPER, invite_email: 'toto2@example.com').
current_user: @master_user tap { |u| u.accept_invite!(accepted_invite_user) }
)
@accepted_invite_member = project.members.invite.find_by_invite_email('toto2@example.com').tap { |u| u.accept_invite!(accepted_invite_user) }
requested_user = create(:user).tap { |u| project.request_access(u) } requested_user = create(:user).tap { |u| project.request_access(u) }
@requested_member = project.requesters.find_by(user_id: requested_user.id) @requested_member = project.requesters.find_by(user_id: requested_user.id)
...@@ -176,39 +171,209 @@ describe Member, models: true do ...@@ -176,39 +171,209 @@ describe Member, models: true do
it { is_expected.to respond_to(:user_email) } it { is_expected.to respond_to(:user_email) }
end end
describe ".add_user" do describe '.add_user' do
let!(:user) { create(:user) } %w[project group].each do |source_type|
let(:project) { create(:project) } context "when source is a #{source_type}" do
let!(:source) { create(source_type, :public) }
let!(:user) { create(:user) }
let!(:admin) { create(:admin) }
context "when called with a user id" do it 'returns a <Source>Member object' do
it "adds the user as a member" do member = described_class.add_user(source, user, :master)
Member.add_user(project.project_members, user.id, ProjectMember::MASTER)
expect(project.users).to include(user) expect(member).to be_a "#{source_type.classify}Member".constantize
end expect(member).to be_persisted
end end
context "when called with a user object" do it 'sets members.created_by to the given current_user' do
it "adds the user as a member" do member = described_class.add_user(source, user, :master, current_user: admin)
Member.add_user(project.project_members, user, ProjectMember::MASTER)
expect(project.users).to include(user) expect(member.created_by).to eq(admin)
end end
end
context "when called with a known user email" do it 'sets members.expires_at to the given expires_at' do
it "adds the user as a member" do member = described_class.add_user(source, user, :master, expires_at: Date.new(2016, 9, 22))
Member.add_user(project.project_members, user.email, ProjectMember::MASTER)
expect(project.users).to include(user) expect(member.expires_at).to eq(Date.new(2016, 9, 22))
end end
end
described_class.access_levels.each do |sym_key, int_access_level|
it "accepts the :#{sym_key} symbol as access level" do
expect(source.users).not_to include(user)
member = described_class.add_user(source, user.id, sym_key)
expect(member.access_level).to eq(int_access_level)
expect(source.users.reload).to include(user)
end
it "accepts the #{int_access_level} integer as access level" do
expect(source.users).not_to include(user)
member = described_class.add_user(source, user.id, int_access_level)
expect(member.access_level).to eq(int_access_level)
expect(source.users.reload).to include(user)
end
end
context 'with no current_user' do
context 'when called with a known user id' do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
described_class.add_user(source, user.id, :master)
expect(source.users.reload).to include(user)
end
end
context 'when called with an unknown user id' do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
described_class.add_user(source, 42, :master)
expect(source.users.reload).not_to include(user)
end
end
context 'when called with a user object' do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
described_class.add_user(source, user, :master)
expect(source.users.reload).to include(user)
end
end
context 'when called with a requester user object' do
before do
source.request_access(user)
end
it 'adds the requester as a member' do
expect(source.users).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
expect { described_class.add_user(source, user, :master) }.
to raise_error(Gitlab::Access::AccessDeniedError)
expect(source.users.reload).not_to include(user)
expect(source.requesters.reload.exists?(user_id: user)).to be_truthy
end
end
context 'when called with a known user email' do
it 'adds the user as a member' do
expect(source.users).not_to include(user)
described_class.add_user(source, user.email, :master)
expect(source.users.reload).to include(user)
end
end
context 'when called with an unknown user email' do
it 'creates an invited member' do
expect(source.users).not_to include(user)
described_class.add_user(source, 'user@example.com', :master)
expect(source.members.invite.pluck(:invite_email)).to include('user@example.com')
end
end
end
context 'when current_user can update member' do
it 'creates the member' do
expect(source.users).not_to include(user)
described_class.add_user(source, user, :master, current_user: admin)
expect(source.users.reload).to include(user)
end
context 'when called with a requester user object' do
before do
source.request_access(user)
end
it 'adds the requester as a member' do
expect(source.users).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
described_class.add_user(source, user, :master, current_user: admin)
expect(source.users.reload).to include(user)
expect(source.requesters.reload.exists?(user_id: user)).to be_falsy
end
end
end
context 'when current_user cannot update member' do
it 'does not create the member' do
expect(source.users).not_to include(user)
member = described_class.add_user(source, user, :master, current_user: user)
expect(source.users.reload).not_to include(user)
expect(member).not_to be_persisted
end
context 'when called with a requester user object' do
before do
source.request_access(user)
end
it 'does not destroy the requester' do
expect(source.users).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
described_class.add_user(source, user, :master, current_user: user)
expect(source.users.reload).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
end
end
end
context 'when member already exists' do
before do
source.add_user(user, :developer)
end
context 'with no current_user' do
it 'updates the member' do
expect(source.users).to include(user)
described_class.add_user(source, user, :master)
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MASTER)
end
end
context 'when current_user can update member' do
it 'updates the member' do
expect(source.users).to include(user)
described_class.add_user(source, user, :master, current_user: admin)
expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MASTER)
end
end
context 'when current_user cannot update member' do
it 'does not update the member' do
expect(source.users).to include(user)
context "when called with an unknown user email" do described_class.add_user(source, user, :master, current_user: user)
it "adds a member invite" do
Member.add_user(project.project_members, "user@example.com", ProjectMember::MASTER)
expect(project.project_members.invite.pluck(:invite_email)).to include("user@example.com") expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER)
end
end
end
end end
end end
end end
......
require 'spec_helper' require 'spec_helper'
describe GroupMember, models: true do describe GroupMember, models: true do
describe '.access_level_roles' do
it 'returns Gitlab::Access.options_with_owner' do
expect(described_class.access_level_roles).to eq(Gitlab::Access.options_with_owner)
end
end
describe '.access_levels' do
it 'returns Gitlab::Access.options_with_owner' do
expect(described_class.access_levels).to eq(Gitlab::Access.sym_options_with_owner)
end
end
describe '.add_users_to_group' do
it 'adds the given users to the given group' do
group = create(:group)
users = create_list(:user, 2)
described_class.add_users_to_group(
group,
[users.first.id, users.second],
described_class::MASTER
)
expect(group.users).to include(users.first, users.second)
end
end
describe 'notifications' do describe 'notifications' do
describe "#after_create" do describe "#after_create" do
it "sends email to user" do it "sends email to user" do
......
...@@ -15,6 +15,26 @@ describe ProjectMember, models: true do ...@@ -15,6 +15,26 @@ describe ProjectMember, models: true do
it { is_expected.to include_module(Gitlab::ShellAdapter) } it { is_expected.to include_module(Gitlab::ShellAdapter) }
end end
describe '.access_level_roles' do
it 'returns Gitlab::Access.options' do
expect(described_class.access_level_roles).to eq(Gitlab::Access.options)
end
end
describe '.add_user' do
context 'when called with the project owner' do
it 'adds the user as a member' do
project = create(:empty_project)
expect(project.users).not_to include(project.owner)
described_class.add_user(project, project.owner, :master, current_user: project.owner)
expect(project.users.reload).to include(project.owner)
end
end
end
describe '#real_source_type' do describe '#real_source_type' do
subject { create(:project_member).real_source_type } subject { create(:project_member).real_source_type }
...@@ -50,7 +70,7 @@ describe ProjectMember, models: true do ...@@ -50,7 +70,7 @@ describe ProjectMember, models: true do
end end
end end
describe :import_team do describe '.import_team' do
before do before do
@project_1 = create :project @project_1 = create :project
@project_2 = create :project @project_2 = create :project
...@@ -81,25 +101,21 @@ describe ProjectMember, models: true do ...@@ -81,25 +101,21 @@ describe ProjectMember, models: true do
end end
describe '.add_users_to_projects' do describe '.add_users_to_projects' do
before do it 'adds the given users to the given projects' do
@project_1 = create :project projects = create_list(:empty_project, 2)
@project_2 = create :project users = create_list(:user, 2)
@user_1 = create :user described_class.add_users_to_projects(
@user_2 = create :user [projects.first.id, projects.second],
[users.first.id, users.second],
ProjectMember.add_users_to_projects( described_class::MASTER)
[@project_1.id, @project_2.id],
[@user_1.id, @user_2.id],
ProjectMember::MASTER
)
end
it { expect(@project_1.users).to include(@user_1) } expect(projects.first.users).to include(users.first)
it { expect(@project_1.users).to include(@user_2) } expect(projects.first.users).to include(users.second)
it { expect(@project_2.users).to include(@user_1) } expect(projects.second.users).to include(users.first)
it { expect(@project_2.users).to include(@user_2) } expect(projects.second.users).to include(users.second)
end
end end
describe '.truncate_teams' do describe '.truncate_teams' do
......
...@@ -287,6 +287,46 @@ describe MergeRequest, models: true do ...@@ -287,6 +287,46 @@ describe MergeRequest, models: true do
end end
end end
describe "#wipless_title" do
['WIP ', 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', ' [WIP] WIP [WIP] WIP: WIP '].each do |wip_prefix|
it "removes the '#{wip_prefix}' prefix" do
wipless_title = subject.title
subject.title = "#{wip_prefix}#{subject.title}"
expect(subject.wipless_title).to eq wipless_title
end
it "is satisfies the #work_in_progress? method" do
subject.title = "#{wip_prefix}#{subject.title}"
subject.title = subject.wipless_title
expect(subject.work_in_progress?).to eq false
end
end
end
describe "#wip_title" do
it "adds the WIP: prefix to the title" do
wip_title = "WIP: #{subject.title}"
expect(subject.wip_title).to eq wip_title
end
it "does not add the WIP: prefix multiple times" do
wip_title = "WIP: #{subject.title}"
subject.title = subject.wip_title
subject.title = subject.wip_title
expect(subject.wip_title).to eq wip_title
end
it "is satisfies the #work_in_progress? method" do
subject.title = subject.wip_title
expect(subject.work_in_progress?).to eq true
end
end
describe '#can_remove_source_branch?' do describe '#can_remove_source_branch?' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
......
...@@ -20,10 +20,10 @@ describe Milestone, models: true do ...@@ -20,10 +20,10 @@ describe Milestone, models: true do
let(:user) { create(:user) } let(:user) { create(:user) }
describe "#title" do describe "#title" do
let(:milestone) { create(:milestone, title: "<b>test</b>") } let(:milestone) { create(:milestone, title: "<b>foo & bar -> 2.2</b>") }
it "sanitizes title" do it "sanitizes title" do
expect(milestone.title).to eq("test") expect(milestone.title).to eq("foo & bar -> 2.2")
end end
end end
......
...@@ -68,7 +68,7 @@ describe Project, models: true do ...@@ -68,7 +68,7 @@ describe Project, models: true do
it { is_expected.to have_many(:forks).through(:forked_project_links) } it { is_expected.to have_many(:forks).through(:forked_project_links) }
describe '#members & #requesters' do describe '#members & #requesters' do
let(:project) { create(:project) } let(:project) { create(:project, :public) }
let(:requester) { create(:user) } let(:requester) { create(:user) }
let(:developer) { create(:user) } let(:developer) { create(:user) }
before do before do
...@@ -836,7 +836,7 @@ describe Project, models: true do ...@@ -836,7 +836,7 @@ describe Project, models: true do
describe 'when a user has access to a project' do describe 'when a user has access to a project' do
before do before do
project.team.add_user(user, Gitlab::Access::MASTER) project.add_user(user, Gitlab::Access::MASTER)
end end
it { is_expected.to eq([project]) } it { is_expected.to eq([project]) }
......
...@@ -137,7 +137,7 @@ describe ProjectTeam, models: true do ...@@ -137,7 +137,7 @@ describe ProjectTeam, models: true do
describe '#find_member' do describe '#find_member' do
context 'personal project' do context 'personal project' do
let(:project) { create(:empty_project) } let(:project) { create(:empty_project, :public) }
let(:requester) { create(:user) } let(:requester) { create(:user) }
before do before do
...@@ -200,7 +200,7 @@ describe ProjectTeam, models: true do ...@@ -200,7 +200,7 @@ describe ProjectTeam, models: true do
let(:requester) { create(:user) } let(:requester) { create(:user) }
context 'personal project' do context 'personal project' do
let(:project) { create(:empty_project) } let(:project) { create(:empty_project, :public) }
context 'when project is not shared with group' do context 'when project is not shared with group' do
before do before do
......
...@@ -64,12 +64,12 @@ describe API::AccessRequests, api: true do ...@@ -64,12 +64,12 @@ describe API::AccessRequests, api: true do
context 'when authenticated as a member' do context 'when authenticated as a member' do
%i[developer master].each do |type| %i[developer master].each do |type|
context "as a #{type}" do context "as a #{type}" do
it 'returns 400' do it 'returns 403' do
expect do expect do
user = public_send(type) user = public_send(type)
post api("/#{source_type.pluralize}/#{source.id}/access_requests", user) post api("/#{source_type.pluralize}/#{source.id}/access_requests", user)
expect(response).to have_http_status(400) expect(response).to have_http_status(403)
end.not_to change { source.requesters.count } end.not_to change { source.requesters.count }
end end
end end
...@@ -87,6 +87,20 @@ describe API::AccessRequests, api: true do ...@@ -87,6 +87,20 @@ describe API::AccessRequests, api: true do
end end
context 'when authenticated as a stranger' do context 'when authenticated as a stranger' do
context "when access request is disabled for the #{source_type}" do
before do
source.update(request_access_enabled: false)
end
it 'returns 403' do
expect do
post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger)
expect(response).to have_http_status(403)
end.not_to change { source.requesters.count }
end
end
it 'returns 201' do it 'returns 201' do
expect do expect do
post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger) post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger)
......
...@@ -15,7 +15,7 @@ describe API::API, api: true do ...@@ -15,7 +15,7 @@ describe API::API, api: true do
let(:milestone) { create(:milestone, title: '1.0.0', project: project) } let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
before do before do
project.team << [user, :reporters] project.team << [user, :reporter]
end end
describe "GET /projects/:id/merge_requests" do describe "GET /projects/:id/merge_requests" do
...@@ -299,7 +299,7 @@ describe API::API, api: true do ...@@ -299,7 +299,7 @@ describe API::API, api: true do
let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) } let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) }
before :each do |each| before :each do |each|
fork_project.team << [user2, :reporters] fork_project.team << [user2, :reporter]
end end
it "returns merge_request" do it "returns merge_request" do
......
...@@ -104,6 +104,14 @@ describe API::API, api: true do ...@@ -104,6 +104,14 @@ describe API::API, api: true do
expect(response).to have_http_status(400) expect(response).to have_http_status(400)
end end
it 'creates a new project with reserved html characters' do
post api("/projects/#{project.id}/milestones", user), title: 'foo & bar 1.1 -> 2.2'
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('foo & bar 1.1 -> 2.2')
expect(json_response['description']).to be_nil
end
end end
describe 'PUT /projects/:id/milestones/:milestone_id' do describe 'PUT /projects/:id/milestones/:milestone_id' do
......
...@@ -14,22 +14,38 @@ describe API::API, 'Settings', api: true do ...@@ -14,22 +14,38 @@ describe API::API, 'Settings', api: true do
expect(json_response['default_projects_limit']).to eq(42) expect(json_response['default_projects_limit']).to eq(42)
expect(json_response['signin_enabled']).to be_truthy expect(json_response['signin_enabled']).to be_truthy
expect(json_response['repository_storage']).to eq('default') expect(json_response['repository_storage']).to eq('default')
expect(json_response['koding_enabled']).to be_falsey
expect(json_response['koding_url']).to be_nil
end end
end end
describe "PUT /application/settings" do describe "PUT /application/settings" do
before do context "custom repository storage type set in the config" do
storages = { 'custom' => 'tmp/tests/custom_repositories' } before do
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) storages = { 'custom' => 'tmp/tests/custom_repositories' }
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
it "updates application settings" do
put api("/application/settings", admin),
default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com'
expect(response).to have_http_status(200)
expect(json_response['default_projects_limit']).to eq(3)
expect(json_response['signin_enabled']).to be_falsey
expect(json_response['repository_storage']).to eq('custom')
expect(json_response['koding_enabled']).to be_truthy
expect(json_response['koding_url']).to eq('http://koding.example.com')
end
end end
it "updates application settings" do context "missing koding_url value when koding_enabled is true" do
put api("/application/settings", admin), it "returns a blank parameter error message" do
default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom' put api("/application/settings", admin), koding_enabled: true
expect(response).to have_http_status(200)
expect(json_response['default_projects_limit']).to eq(3) expect(response).to have_http_status(400)
expect(json_response['signin_enabled']).to be_falsey expect(json_response['message']).to have_key('koding_url')
expect(json_response['repository_storage']).to eq('custom') expect(json_response['message']['koding_url']).to include "can't be blank"
end
end end
end end
end end
require 'spec_helper'
describe Members::RequestAccessService, services: true do
let(:user) { create(:user) }
let(:project) { create(:project, :private) }
let(:group) { create(:group, :private) }
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
it 'raises Gitlab::Access::AccessDeniedError' do
expect { described_class.new(source, user).execute }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
shared_examples 'a service creating a access request' do
it 'succeeds' do
expect { described_class.new(source, user).execute }.to change { source.requesters.count }.by(1)
end
it 'returns a <Source>Member' do
member = described_class.new(source, user).execute
expect(member).to be_a "#{source.class.to_s}Member".constantize
expect(member.requested_at).to be_present
end
end
context 'when source is nil' do
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { nil }
end
end
context 'when current user cannot request access to the project' do
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { project }
end
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { group }
end
end
context 'when current user can request access to the project' do
before do
project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
group.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
end
it_behaves_like 'a service creating a access request' do
let(:source) { project }
end
it_behaves_like 'a service creating a access request' do
let(:source) { group }
end
end
end
...@@ -38,6 +38,30 @@ describe MergeRequests::MergeService, services: true do ...@@ -38,6 +38,30 @@ describe MergeRequests::MergeService, services: true do
end end
end end
context 'closes related todos' do
let(:merge_request) { create(:merge_request, assignee: user, author: user) }
let(:project) { merge_request.project }
let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') }
let!(:todo) do
create(:todo, :assigned,
project: project,
author: user,
user: user,
target: merge_request)
end
before do
allow(service).to receive(:execute_hooks)
perform_enqueued_jobs do
service.execute(merge_request)
todo.reload
end
end
it { expect(todo).to be_done }
end
context 'remove source branch by author' do context 'remove source branch by author' do
let(:service) do let(:service) do
merge_request.merge_params['force_remove_source_branch'] = '1' merge_request.merge_params['force_remove_source_branch'] = '1'
......
...@@ -79,8 +79,8 @@ describe MergeRequests::RefreshService, services: true do ...@@ -79,8 +79,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_merged } it { expect(@merge_request).to be_merged }
it { expect(@fork_merge_request).to be_merged } it { expect(@fork_merge_request).to be_merged }
it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') } it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') }
it { expect(@build_failed_todo).to be_pending } it { expect(@build_failed_todo).to be_done }
it { expect(@fork_build_failed_todo).to be_pending } it { expect(@fork_build_failed_todo).to be_done }
end end
context 'manual merge of source branch' do context 'manual merge of source branch' do
...@@ -99,8 +99,8 @@ describe MergeRequests::RefreshService, services: true do ...@@ -99,8 +99,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request.diffs.size).to be > 0 } it { expect(@merge_request.diffs.size).to be > 0 }
it { expect(@fork_merge_request).to be_merged } it { expect(@fork_merge_request).to be_merged }
it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') } it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') }
it { expect(@build_failed_todo).to be_pending } it { expect(@build_failed_todo).to be_done }
it { expect(@fork_build_failed_todo).to be_pending } it { expect(@fork_build_failed_todo).to be_done }
end end
context 'push to fork repo source branch' do context 'push to fork repo source branch' do
...@@ -149,8 +149,8 @@ describe MergeRequests::RefreshService, services: true do ...@@ -149,8 +149,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_merged } it { expect(@merge_request).to be_merged }
it { expect(@fork_merge_request).to be_open } it { expect(@fork_merge_request).to be_open }
it { expect(@fork_merge_request.notes).to be_empty } it { expect(@fork_merge_request.notes).to be_empty }
it { expect(@build_failed_todo).to be_pending } it { expect(@build_failed_todo).to be_done }
it { expect(@fork_build_failed_todo).to be_pending } it { expect(@fork_build_failed_todo).to be_done }
end end
context 'push new branch that exists in a merge request' do context 'push new branch that exists in a merge request' do
......
...@@ -962,6 +962,20 @@ describe NotificationService, services: true do ...@@ -962,6 +962,20 @@ describe NotificationService, services: true do
should_not_email(@u_lazy_participant) should_not_email(@u_lazy_participant)
end end
it "notifies the merger when merge_when_build_succeeds is true" do
merge_request.merge_when_build_succeeds = true
notification.merge_mr(merge_request, @u_watcher)
should_email(@u_watcher)
end
it "does not notify the merger when merge_when_build_succeeds is false" do
merge_request.merge_when_build_succeeds = false
notification.merge_mr(merge_request, @u_watcher)
should_not_email(@u_watcher)
end
context 'participating' do context 'participating' do
context 'by assignee' do context 'by assignee' do
before do before do
......
...@@ -165,6 +165,23 @@ describe SlashCommands::InterpretService, services: true do ...@@ -165,6 +165,23 @@ describe SlashCommands::InterpretService, services: true do
end end
end end
shared_examples 'wip command' do
it 'returns wip_event: "wip" if content contains /wip' do
_, updates = service.execute(content, issuable)
expect(updates).to eq(wip_event: 'wip')
end
end
shared_examples 'unwip command' do
it 'returns wip_event: "unwip" if content contains /wip' do
issuable.update(title: issuable.wip_title)
_, updates = service.execute(content, issuable)
expect(updates).to eq(wip_event: 'unwip')
end
end
shared_examples 'empty command' do shared_examples 'empty command' do
it 'populates {} if content contains an unsupported command' do it 'populates {} if content contains an unsupported command' do
_, updates = service.execute(content, issuable) _, updates = service.execute(content, issuable)
...@@ -376,6 +393,16 @@ describe SlashCommands::InterpretService, services: true do ...@@ -376,6 +393,16 @@ describe SlashCommands::InterpretService, services: true do
let(:issuable) { issue } let(:issuable) { issue }
end end
it_behaves_like 'wip command' do
let(:content) { '/wip' }
let(:issuable) { merge_request }
end
it_behaves_like 'unwip command' do
let(:content) { '/wip' }
let(:issuable) { merge_request }
end
it_behaves_like 'empty command' do it_behaves_like 'empty command' do
let(:content) { '/remove_due_date' } let(:content) { '/remove_due_date' }
let(:issuable) { merge_request } let(:issuable) { merge_request }
......
...@@ -40,6 +40,12 @@ describe SystemNoteService, services: true do ...@@ -40,6 +40,12 @@ describe SystemNoteService, services: true do
describe 'note body' do describe 'note body' do
let(:note_lines) { subject.note.split("\n").reject(&:blank?) } let(:note_lines) { subject.note.split("\n").reject(&:blank?) }
describe 'comparison diff link line' do
it 'adds the comparison text' do
expect(note_lines[2]).to match "[Compare with previous version]"
end
end
context 'without existing commits' do context 'without existing commits' do
it 'adds a message header' do it 'adds a message header' do
expect(note_lines[0]).to eq "Added #{new_commits.size} commits:" expect(note_lines[0]).to eq "Added #{new_commits.size} commits:"
......
RSpec::Matchers.define :have_issuable_counts do |opts|
match do |actual|
expected_counts = opts.map do |state, count|
"#{state.to_s.humanize} #{count}"
end
actual.within '.issues-state-filters' do
expected_counts.each do |expected_count|
expect(actual).to have_content(expected_count)
end
end
end
description do
"displays the following issuable counts: #{expected_counts.inspect}"
end
failure_message do
"expected the following issuable counts: #{expected_counts.inspect} to be displayed"
end
end
require 'spec_helper'
describe 'ci/lints/show' do
let(:content) do
{
build_template: {
script: './build.sh',
tags: ['dotnet'],
only: ['test@dude/repo'],
except: ['deploy'],
environment: 'testing'
}
}
end
let(:config_processor) { Ci::GitlabCiYamlProcessor.new(YAML.dump(content)) }
context 'when the content is valid' do
before do
assign(:status, true)
assign(:builds, config_processor.builds)
assign(:stages, config_processor.stages)
assign(:jobs, config_processor.jobs)
end
it 'shows the correct values' do
render
expect(rendered).to have_content('Tag list: dotnet')
expect(rendered).to have_content('Refs only: test@dude/repo')
expect(rendered).to have_content('Refs except: deploy')
expect(rendered).to have_content('Environment: testing')
expect(rendered).to have_content('When: on_success')
end
end
context 'when the content is invalid' do
before do
assign(:status, false)
assign(:error, 'Undefined error')
end
it 'shows error message' do
render
expect(rendered).to have_content('Status: syntax is incorrect')
expect(rendered).to have_content('Error: Undefined error')
expect(rendered).not_to have_content('Tag list:')
end
end
end
...@@ -41,4 +41,17 @@ describe 'projects/merge_requests/show.html.haml' do ...@@ -41,4 +41,17 @@ describe 'projects/merge_requests/show.html.haml' do
expect(rendered).to have_css('a', visible: false, text: 'Close') expect(rendered).to have_css('a', visible: false, text: 'Close')
end end
end end
context 'when the merge request is open' do
it 'closes the merge request if the source project does not exist' do
closed_merge_request.update_attributes(state: 'open')
fork_project.destroy
render
expect(closed_merge_request.reload.state).to eq('closed')
expect(rendered).to have_css('a', visible: false, text: 'Reopen')
expect(rendered).to have_css('a', visible: false, text: 'Close')
end
end
end end
...@@ -4,7 +4,7 @@ deps ...@@ -4,7 +4,7 @@ deps
*.beam *.beam
*.plt *.plt
erl_crash.dump erl_crash.dump
ebin ebin/*.beam
rel/example_project rel/example_project
.concrete/DEV_MODE .concrete/DEV_MODE
.rebar .rebar
...@@ -8,3 +8,6 @@ ...@@ -8,3 +8,6 @@
# Linux trash folder which might appear on any partition or disk # Linux trash folder which might appear on any partition or disk
.Trash-* .Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
...@@ -25,3 +25,6 @@ _testmain.go ...@@ -25,3 +25,6 @@ _testmain.go
# Output of the go coverage tool, specifically when used with LiteIDE # Output of the go coverage tool, specifically when used with LiteIDE
*.out *.out
# external packages folder
vendor/
...@@ -39,3 +39,6 @@ jspm_packages ...@@ -39,3 +39,6 @@ jspm_packages
# Optional REPL history # Optional REPL history
.node_repl_history .node_repl_history
# Output of 'npm pack'
*.tgz
...@@ -192,3 +192,6 @@ TSWLatexianTemp* ...@@ -192,3 +192,6 @@ TSWLatexianTemp*
# KBibTeX # KBibTeX
*~[0-9]* *~[0-9]*
# auto folder when using emacs and auctex
/auto/*
...@@ -110,6 +110,10 @@ _TeamCity* ...@@ -110,6 +110,10 @@ _TeamCity*
# DotCover is a Code Coverage Tool # DotCover is a Code Coverage Tool
*.dotCover *.dotCover
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch # NCrunch
_NCrunch_* _NCrunch_*
.*crunch*.local.xml .*crunch*.local.xml
...@@ -189,6 +193,7 @@ ClientBin/ ...@@ -189,6 +193,7 @@ ClientBin/
*~ *~
*.dbmdl *.dbmdl
*.dbproj.schemaview *.dbproj.schemaview
*.jfm
*.pfx *.pfx
*.publishsettings *.publishsettings
node_modules/ node_modules/
...@@ -258,3 +263,6 @@ paket-files/ ...@@ -258,3 +263,6 @@ paket-files/
# Python Tools for Visual Studio (PTVS) # Python Tools for Visual Studio (PTVS)
__pycache__/ __pycache__/
*.pyc *.pyc
# Cake - Uncomment if you are using it
# tools/
image: ruby:2.3-alpine
test:
script: ruby verify_templates.rb
# This template uses the java:8 docker image because there isn't any
# official Gradle image at this moment
#
# This is the Gradle build system for JVM applications
# https://gradle.org/
# https://github.com/gradle/gradle
image: java:8
# Make the gradle wrapper executable. This essentially downloads a copy of
# Gradle to build the project with.
# https://docs.gradle.org/current/userguide/gradle_wrapper.html
# It is expected that any modern gradle project has a wrapper
before_script:
- chmod +x gradlew
# We redirect the gradle user home using -g so that it caches the
# wrapper and dependencies.
# https://docs.gradle.org/current/userguide/gradle_command_line.html
#
# Unfortunately it also caches the build output so
# cleaning removes reminants of any cached builds.
# The assemble task actually builds the project.
# If it fails here, the tests can't run.
build:
stage: build
script:
- ./gradlew -g /cache/.gradle clean assemble
allow_failure: false
# Use the generated build output to run the tests.
test:
stage: test
script:
- ./gradlew -g /cache./gradle check
# An example .gitlab-ci.yml file to test (and optionally report the coverage
# results of) your [Julia][1] packages. Please refer to the [documentation][2]
# for more information about package development in Julia.
#
# Here, it is assumed that your Julia package is named `MyPackage`. Change it to
# whatever name you have given to your package.
#
# [1]: http://julialang.org/
# [2]: http://julia.readthedocs.org/
# Below is the template to run your tests in Julia
.test_template: &test_definition
# Uncomment below if you would like to run the tests on specific references
# only, such as the branches `master`, `development`, etc.
# only:
# - master
# - development
script:
# Let's run the tests. Substitute `coverage = false` below, if you do not
# want coverage results.
- /opt/julia/bin/julia -e 'Pkg.clone(pwd()); Pkg.test("MyPackage",
coverage = true)'
# Comment out below if you do not want coverage results.
- /opt/julia/bin/julia -e 'Pkg.add("Coverage"); cd(Pkg.dir("MyPackage"));
using Coverage; cl, tl = get_summary(process_folder());
println("(", cl/tl*100, "%) covered")'
# Name a test and select an appropriate image.
test:0.4.6:
image: julialang/julia:v0.4.6
<<: *test_definition
# Maybe you would like to test your package against the development branch:
test:0.5.0-dev:
image: julialang/julia:v0.5.0-dev
# ... allowing for failures, since we are testing against the development
# branch:
allow_failure: true
<<: *test_definition
# REMARK: Do not forget to enable the coverage feature for your project, if you
# are using code coverage reporting above. This can be done by
#
# - Navigating to the `CI/CD Pipelines` settings of your project,
# - Copying and pasting the default `Simplecov` regex example provided, i.e.,
# `\(\d+.\d+\%\) covered` in the `test coverage parsing` textfield.
#
# WARNING: This template is using the `julialang/julia` images from [Docker
# Hub][3]. One can use custom Julia images and/or the official ones found
# in the same place. However, care must be taken to correctly locate the binary
# file (`/opt/julia/bin/julia` above), which is usually given on the image's
# description page.
#
# [3]: http://hub.docker.com/
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