diff options
63 files changed, 876 insertions, 327 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index b054675d677..5bd31ccf329 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -767,33 +767,26 @@ Rails/ScopeArgs: RSpec/AnyInstance: Enabled: false -# Check for expectations where `be(...)` can replace `eql(...)`. -RSpec/BeEql: - Enabled: false - -# Check that the first argument to the top level describe is a constant. +# Check that the first argument to the top level describe is the tested class or +# module. RSpec/DescribeClass: Enabled: false -# Checks that tests use `described_class`. -RSpec/DescribedClass: - Enabled: false - -# Checks that the second argument to `describe` specifies a method. +# Use `described_class` for tested class / module. RSpec/DescribeMethod: Enabled: false -# Checks if an example group does not include any tests. -RSpec/EmptyExampleGroup: +# Checks that the second argument to top level describe is the tested method +# name. +RSpec/DescribedClass: Enabled: false - CustomIncludeMethods: [] -# Checks for long examples. +# Checks for long example. RSpec/ExampleLength: Enabled: false Max: 5 -# Checks that example descriptions do not start with "should". +# Do not use should when describing your tests. RSpec/ExampleWording: Enabled: false CustomTransform: @@ -802,10 +795,6 @@ RSpec/ExampleWording: not: does not IgnoredWords: [] -# Checks for `expect(...)` calls containing literal values. -RSpec/ExpectActual: - Enabled: false - # Checks the file and folder naming of the spec file. RSpec/FilePath: Enabled: false @@ -817,65 +806,19 @@ RSpec/FilePath: RSpec/Focus: Enabled: true -# Checks the arguments passed to `before`, `around`, and `after`. -RSpec/HookArgument: - Enabled: false - EnforcedStyle: implicit - -# Check that a consistent implict expectation style is used. -# TODO (rspeicher): Available in rubocop-rspec 1.8.0 -# RSpec/ImplicitExpect: -# Enabled: true -# EnforcedStyle: is_expected - # Checks for the usage of instance variables. RSpec/InstanceVariable: Enabled: false -# Checks for `subject` definitions that come after `let` definitions. -RSpec/LeadingSubject: - Enabled: false - -# Checks unreferenced `let!` calls being used for test setup. -RSpec/LetSetup: - Enabled: false - -# Check that chains of messages are not being stubbed. -RSpec/MessageChain: - Enabled: false - -# Checks for consistent message expectation style. -RSpec/MessageExpectation: - Enabled: false - EnforcedStyle: allow - -# Checks for multiple top level describes. +# Checks for multiple top-level describes. RSpec/MultipleDescribes: Enabled: false -# Checks if examples contain too many `expect` calls. -RSpec/MultipleExpectations: - Enabled: false - Max: 1 - -# Checks for explicitly referenced test subjects. -RSpec/NamedSubject: - Enabled: false - -# Checks for nested example groups. -RSpec/NestedGroups: - Enabled: false - MaxNesting: 2 - -# Checks for consistent method usage for negating expectations. +# Enforces the usage of the same method on all negative message expectations. RSpec/NotToNot: EnforcedStyle: not_to Enabled: true -# Checks for stubbed test subjects. -RSpec/SubjectStub: - Enabled: false - # Prefer using verifying doubles over normal doubles. RSpec/VerifiedDoubles: Enabled: false diff --git a/CHANGELOG b/CHANGELOG index 9370bdd17d2..6e9567e7e20 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -24,11 +24,13 @@ v 8.12.0 (unreleased) - Cycle analytics (first iteration) !5986 - Remove vendor prefixes for linear-gradient CSS (ClemMakesApps) - Move pushes_since_gc from the database to Redis + - Limit number of shown environments on Merge Request: show only environments for target_branch, source_branch and tags - Add font color contrast to external label in admin area (ClemMakesApps) - Change logo animation to CSS (ClemMakesApps) - Instructions for enabling Git packfile bitmaps !6104 - Use Search::GlobalService.new in the `GET /projects/search/:query` endpoint - Fix long comments in diffs messing with table width + - Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman) - Fix pagination on user snippets page - Run CI builds with the permissions of users !5735 - Fix sorting of issues in API @@ -40,14 +42,17 @@ v 8.12.0 (unreleased) - Escape search term before passing it to Regexp.new !6241 (winniehell) - Fix pinned sidebar behavior in smaller viewports !6169 - Fix file permissions change when updating a file on the Gitlab UI !5979 + - Added horizontal padding on build page sidebar on code coverage block. !6196 (Vitaly Baev) - Change merge_error column from string to text type - Reduce contributions calendar data payload (ClemMakesApps) + - Show all pipelines for merge requests even from discarded commits !6414 - Replace contributions calendar timezone payload with dates (ClemMakesApps) - Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel) - Enable pipeline events by default !6278 - Move parsing of sidekiq ps into helper !6245 (pascalbetz) - Added go to issue boards keyboard shortcut - Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel) + - Emoji can be awarded on Snippets !4456 - Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling) - Fix blame table layout width - Spec testing if issue authors can read issues on private projects @@ -165,7 +170,6 @@ v 8.12.0 (unreleased) - Add notification_settings API calls !5632 (mahcsig) - Remove duplication between project builds and admin builds view !5680 (Katarzyna Kobierska Ula Budziszewska) - Fix URLs with anchors in wiki !6300 (houqp) - - Use a ConnectionPool for Rails.cache on Sidekiq servers - Deleting source project with existing fork link will close all related merge requests !6177 (Katarzyna Kobierska Ula Budziszeska) - Return 204 instead of 404 for /ci/api/v1/builds/register.json if no builds are scheduled for a runner !6225 - Fix Gitlab::Popen.popen thread-safety issue @@ -176,6 +180,11 @@ v 8.12.0 (unreleased) - Fix non-master branch readme display in tree view - Add UX improvements for merge request version diffs +v 8.11.7 + - Avoid conflict with admin labels when importing GitHub labels. !6158 + - Restores `fieldName` to allow only string values in `gl_dropdown.js`. !6234 + - Allow the Rails cookie to be used for API authentication. + v 8.11.6 - Fix unnecessary horizontal scroll area in pipeline visualizations. !6005 - Make merge conflict file size limit 200 KB, to match the docs. !6052 @@ -390,6 +399,9 @@ v 8.11.0 - Update gitlab_git gem to 10.4.7 - Simplify SQL queries of marking a todo as done +v 8.10.10 + - Allow the Rails cookie to be used for API authentication. + v 8.10.9 - Exclude some pending or inactivated rows in Member scopes @@ -623,6 +635,9 @@ v 8.10.0 - Show tooltip on GitLab export link in new project page - Fix import_data wrongly saved as a result of an invalid import_url !5206 +v 8.9.10 + - Allow the Rails cookie to be used for API authentication. + v 8.9.9 - Exclude some pending or inactivated rows in Member scopes diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 1545d966571..40c341bdcdb 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -3.5.0 +3.6.0 @@ -298,7 +298,7 @@ group :development, :test do gem 'spring-commands-teaspoon', '~> 0.0.2' gem 'rubocop', '~> 0.42.0', require: false - gem 'rubocop-rspec', '~> 1.7.0', require: false + gem 'rubocop-rspec', '~> 1.5.0', require: false gem 'scss_lint', '~> 0.47.0', require: false gem 'haml_lint', '~> 0.18.2', require: false gem 'simplecov', '0.12.0', require: false diff --git a/Gemfile.lock b/Gemfile.lock index c9b4b30c1f2..a5a2e5785ac 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -626,8 +626,8 @@ GEM rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) - rubocop-rspec (1.7.0) - rubocop (>= 0.42.0) + rubocop-rspec (1.5.0) + rubocop (>= 0.40.0) ruby-fogbugz (0.2.1) crack (~> 0.4) ruby-prof (0.15.9) @@ -943,7 +943,7 @@ DEPENDENCIES rspec-rails (~> 3.5.0) rspec-retry (~> 0.4.5) rubocop (~> 0.42.0) - rubocop-rspec (~> 1.7.0) + rubocop-rspec (~> 1.5.0) ruby-fogbugz (~> 0.2.1) ruby-prof (~> 0.15.9) sanitize (~> 2.0) diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index f5223207f3a..2432ddb72f4 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -129,8 +129,6 @@ position: relative; .avatar-holder { - margin-bottom: 16px; - .avatar, .identicon { margin: 0 auto; float: none; @@ -143,13 +141,7 @@ .cover-title { color: $gl-header-color; - margin: 0; - font-size: 24px; - font-weight: normal; - margin-bottom: 10px; - color: #4c4e54; font-size: 23px; - line-height: 1.1; h1 { color: $gl-gray-dark; @@ -213,6 +205,9 @@ } } } + &.user-cover-block { + padding: 24px 0 0; + } .group-info { diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 553768b2e68..ea43f4afc37 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -99,8 +99,7 @@ .top-area { @include clearfix; - - border-bottom: 1px solid #eee; + border-bottom: 1px solid $btn-gray-hover; .nav-text { padding-top: 16px; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 2582cde5a71..9f2d53d5206 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -204,7 +204,7 @@ body { } h1, h2, h3, h4, h5, h6 { - color: $gl-header-color; + color: $gl-title-color; font-weight: 600; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 9f563a4de35..2f5e3ec8e44 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -102,7 +102,7 @@ $gl-grayish-blue: #7f8fa4; $gl-gray: $gl-text-color; $gl-gray-dark: #313236; $gl-gray-light: $gl-placeholder-color; -$gl-header-color: $gl-title-color; +$gl-header-color: #4c4e54; /* * Lists diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index c879074c7fe..a5a260d4c8f 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -109,6 +109,10 @@ width: 100%; } + .block-first { + padding: 5px 16px 11px; + } + .js-build-variable { color: $code-color; } diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index b657ca47d38..732dc645c66 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -55,3 +55,16 @@ } } } + +.groups-header { + + @media (min-width: $screen-sm-min) { + .nav-links { + width: 35%; + } + + .nav-controls { + width: 65%; + } + } +} diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 6f58203f49c..0fcdaf94a21 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -93,8 +93,9 @@ .profile-user-bio { // Limits the width of the user bio for readability. - max-width: 750px; - margin: auto; + max-width: 600px; + margin: 15px auto 0; + padding: 0 16px; } .user-avatar-button { @@ -212,6 +213,28 @@ } .user-profile { + .cover-controls a { + margin-left: 5px; + } + .profile-header { + margin: 0 auto; + .avatar-holder { + width: 90px; + display: inline-block; + } + .user-info { + display: inline-block; + text-align: left; + vertical-align: middle; + margin-left: 15px; + .handle { + color: $gl-gray-light; + } + .member-date { + margin-bottom: 4px; + } + } + } @media (max-width: $screen-xs-max) { .cover-block { padding-top: 20px; @@ -219,16 +242,26 @@ .cover-controls { position: static; + padding: 0 16px; margin-bottom: 20px; + display: -webkit-flex; + display: flex; .btn { - display: inline-block; - width: 46%; + -webkit-flex-grow: 1; + flex-grow: 1; + &:first-child { + margin-left: 0; + } } } } } +.user-profile-nav { + margin-top: 15px; +} + table.u2f-registrations { th:not(:last-child), td:not(:last-child) { border-right: solid 1px transparent; diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index 5270aea4e79..4d5df566d9b 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -12,11 +12,18 @@ .snippet-file-content { border-radius: 3px; + margin-bottom: $gl-padding; + .btn-clipboard { @extend .btn; } } +.project-snippets .awards { + border-bottom: 1px solid $table-border-color; + padding-bottom: $gl-padding; +} + .snippet-title { font-size: 24px; font-weight: 600; diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb index 172d5344b7a..3717c49f272 100644 --- a/app/controllers/concerns/toggle_award_emoji.rb +++ b/app/controllers/concerns/toggle_award_emoji.rb @@ -10,7 +10,9 @@ module ToggleAwardEmoji if awardable.user_can_award?(current_user, name) awardable.toggle_award_emoji(name, current_user) - TodoService.new.new_award_emoji(to_todoable(awardable), current_user) + + todoable = to_todoable(awardable) + TodoService.new.new_award_emoji(todoable, current_user) if todoable render json: { ok: true } else @@ -24,8 +26,10 @@ module ToggleAwardEmoji case awardable when Note awardable.noteable - else + when MergeRequest, Issue awardable + when Snippet + nil end end diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 17ceefec3b8..e290a0eadda 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -1,6 +1,8 @@ class Projects::SnippetsController < Projects::ApplicationController + include ToggleAwardEmoji + before_action :module_enabled - before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] + before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji] # Allow read any snippet before_action :authorize_read_project_snippet!, except: [:new, :create, :index] @@ -80,6 +82,7 @@ class Projects::SnippetsController < Projects::ApplicationController def snippet @snippet ||= @project.snippets.find(params[:id]) end + alias_method :awardable, :snippet def authorize_read_project_snippet! return render_404 unless can?(current_user, :read_project_snippet, @snippet) diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 2a17c1f34db..d198782138a 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,4 +1,6 @@ class SnippetsController < ApplicationController + include ToggleAwardEmoji + before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] # Allow read snippet @@ -85,6 +87,7 @@ class SnippetsController < ApplicationController PersonalSnippet.find(params[:id]) end end + alias_method :awardable, :snippet def authorize_read_snippet! authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet) diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb new file mode 100644 index 00000000000..aa134cea31c --- /dev/null +++ b/app/helpers/award_emoji_helper.rb @@ -0,0 +1,9 @@ +module AwardEmojiHelper + def toggle_award_url(awardable) + if @project + url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) + else + url_for([:toggle_award_emoji, awardable]) + end + end +end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 5b71113feb9..670a7ca36f4 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -70,6 +70,10 @@ module GitlabRoutingHelper namespace_project_runner_path(@project.namespace, @project, runner, *args) end + def environment_path(environment, *args) + namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args) + end + def issue_path(entity, *args) namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args) end @@ -102,6 +106,14 @@ module GitlabRoutingHelper end end + def toggle_award_emoji_personal_snippet_path(*args) + toggle_award_emoji_snippet_path(*args) + end + + def toggle_award_emoji_namespace_project_project_snippet_path(*args) + toggle_award_emoji_namespace_project_snippet_path(*args) + end + ## Members def project_members_url(project, *args) namespace_project_project_members_url(project.namespace, project) diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index d8d4575bb4d..073ac4c1b65 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -71,6 +71,12 @@ module Awardable end end + def user_authored?(current_user) + author = self.respond_to?(:author) ? self.author : self.user + + author == current_user + end + def awarded_emoji?(emoji_name, current_user) award_emoji.where(name: emoji_name, user: current_user).exists? end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 1650ac9fcbe..ff465d2c745 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -200,10 +200,6 @@ module Issuable end end - def user_authored?(user) - user == author - end - def subscribed_without_subscriptions?(user) participants(user).include?(user) end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 616efaf3c42..2dcf7f89bfc 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -670,9 +670,12 @@ class MergeRequest < ActiveRecord::Base def environments return [] unless diff_head_commit - target_project.environments.select do |environment| - environment.includes_commit?(diff_head_commit) - end + environments = source_project.environments_for( + source_branch, diff_head_commit) + environments += target_project.environments_for( + target_branch, diff_head_commit, with_tags: true) + + environments.uniq end def state_human_name @@ -761,10 +764,23 @@ class MergeRequest < ActiveRecord::Base end def all_pipelines - @all_pipelines ||= - if diff_head_sha && source_project - source_project.pipelines.order(id: :desc).where(sha: commits_sha, ref: source_branch) - end + return unless source_project + + @all_pipelines ||= begin + sha = if persisted? + all_commits_sha + else + diff_head_sha + end + + source_project.pipelines.order(id: :desc). + where(sha: sha, ref: source_branch) + end + end + + # Note that this could also return SHA from now dangling commits + def all_commits_sha + merge_request_diffs.flat_map(&:commits_sha).uniq end def merge_commit diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 18c583add88..7362886e9f5 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -117,6 +117,14 @@ class MergeRequestDiff < ActiveRecord::Base project.commit(head_commit_sha) end + def commits_sha + if @commits + commits.map(&:sha) + else + st_commits.map { |commit| commit[:id] } + end + end + def diff_refs return unless start_commit_sha || base_commit_sha diff --git a/app/models/note.rb b/app/models/note.rb index b94e3cff2ce..f2656df028b 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -223,10 +223,6 @@ class Note < ActiveRecord::Base end end - def user_authored?(user) - user == author - end - def award_emoji? can_be_award_emoji? && contains_emoji_only? end diff --git a/app/models/project.rb b/app/models/project.rb index d7f20070be0..7265cb55594 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1293,6 +1293,22 @@ class Project < ActiveRecord::Base Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) } end + def environments_for(ref, commit, with_tags: false) + environment_ids = deployments.group(:environment_id). + select(:environment_id) + + environment_ids = + if with_tags + environment_ids.where('ref=? OR tag IS TRUE', ref) + else + environment_ids.where(ref: ref) + end + + environments.where(id: environment_ids).select do |environment| + environment.includes_commit?(commit) + end + end + private def pushes_since_gc_redis_key diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 9c602c582bd..8c9534c3565 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -22,6 +22,12 @@ class ProjectFeature < ActiveRecord::Base belongs_to :project + default_value_for :builds_access_level, value: ENABLED, allows_nil: false + default_value_for :issues_access_level, value: ENABLED, allows_nil: false + default_value_for :merge_requests_access_level, value: ENABLED, allows_nil: false + default_value_for :snippets_access_level, value: ENABLED, allows_nil: false + default_value_for :wiki_access_level, value: ENABLED, allows_nil: false + def feature_available?(feature, user) raise ArgumentError, 'invalid project feature' unless FEATURES.include?(feature) diff --git a/app/models/project_team.rb b/app/models/project_team.rb index ab6ea2aae36..d9ce5088903 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -163,7 +163,7 @@ class ProjectTeam # Each group produces a list of maximum access level per user. We take the # max of the values produced by each group. - if project.invited_groups.any? && project.allowed_to_share_with_group? + if project_shared_with_group? project.project_group_links.each do |group_link| invited_access = max_invited_level_for_users(group_link, user_ids) merge_max!(access, invited_access) @@ -200,43 +200,17 @@ class ProjectTeam def fetch_members(level = nil) project_members = project.members group_members = group ? group.members : [] - invited_members = [] - - if project.invited_groups.any? && project.allowed_to_share_with_group? - project.project_group_links.includes(group: [:group_members]).each do |group_link| - invited_group = group_link.group - im = invited_group.members - - if level - int_level = GroupMember.access_level_roles[level.to_s.singularize.titleize] - - # Skip group members if we ask for masters - # but max group access is developers - next if int_level > group_link.group_access - - # If we ask for developers and max - # group access is developers we need to provide - # both group master, developers as devs - if int_level == group_link.group_access - im.where("access_level >= ?)", group_link.group_access) - else - im.send(level) - end - end - - invited_members << im - end - - invited_members = invited_members.flatten.compact - end if level - project_members = project_members.send(level) - group_members = group_members.send(level) if group + project_members = project_members.public_send(level) + group_members = group_members.public_send(level) if group end user_ids = project_members.pluck(:user_id) + + invited_members = fetch_invited_members(level) user_ids.push(*invited_members.map(&:user_id)) if invited_members.any? + user_ids.push(*group_members.pluck(:user_id)) if group User.where(id: user_ids) @@ -249,4 +223,38 @@ class ProjectTeam def merge_max!(first_hash, second_hash) first_hash.merge!(second_hash) { |_key, old, new| old > new ? old : new } end + + def project_shared_with_group? + project.invited_groups.any? && project.allowed_to_share_with_group? + end + + def fetch_invited_members(level = nil) + invited_members = [] + + return invited_members unless project_shared_with_group? + + project.project_group_links.includes(group: [:group_members]).each do |link| + invited_group_members = link.group.members + + if level + numeric_level = GroupMember.access_level_roles[level.to_s.singularize.titleize] + + # If we're asked for a level that's higher than the group's access, + # there's nothing left to do + next if numeric_level > link.group_access + + # Make sure we include everyone _above_ the requested level as well + invited_group_members = + if numeric_level == link.group_access + invited_group_members.where("access_level >= ?", link.group_access) + else + invited_group_members.public_send(level) + end + end + + invited_members << invited_group_members + end + + invited_members.flatten.compact + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 772c62a4124..51557228ab9 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -840,7 +840,7 @@ class Repository def get_committer_and_author(user, email: nil, name: nil) committer = user_to_committer(user) - author = name && email ? Gitlab::Git::committer_hash(email: email, name: name) : committer + author = Gitlab::Git::committer_hash(email: email, name: name) || committer { author: author, diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 5ec933601ac..8a1730f3f36 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -4,6 +4,7 @@ class Snippet < ActiveRecord::Base include Participable include Referable include Sortable + include Awardable default_value_for :visibility_level, Snippet::PRIVATE diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index d929364fc96..0d79ca7dc52 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -49,28 +49,6 @@ = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control') %span.help-block#clone-protocol-help Allow only the selected protocols to be used for Git access. - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :version_check_enabled do - = f.check_box :version_check_enabled - Version check enabled - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :email_author_in_body do - = f.check_box :email_author_in_body - Include author name in notification email body - .help-block - Some email servers do not support overriding the email sender name. - Enable this option to include the name of the author of the issue, - merge request or comment in the email body instead. - .form-group - = f.label :admin_notification_email, class: 'control-label col-sm-2' - .col-sm-10 - = f.text_field :admin_notification_email, class: 'form-control' - .help-block - Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area. %fieldset %legend Account and Limit Settings @@ -341,6 +319,15 @@ %a{ href: 'http://www.akismet.com', target: 'blank'} http://www.akismet.com %fieldset + %legend Abuse reports + .form-group + = f.label :admin_notification_email, 'Abuse reports notification email', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :admin_notification_email, class: 'form-control' + .help-block + Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area. + + %fieldset %legend Error Reporting and Logging %p These settings require a restart to take effect. @@ -407,6 +394,29 @@ = succeed "." do = link_to "Koding administration documentation", help_page_path("administration/integration/koding") + %fieldset + %legend Usage statistics + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :version_check_enabled do + = f.check_box :version_check_enabled + Version check enabled + .help-block + Let GitLab inform you when an update is available. + + %fieldset + %legend Email + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :email_author_in_body do + = f.check_box :email_author_in_body + Include author name in notification email body + .help-block + Some email servers do not support overriding the email sender name. + Enable this option to include the name of the author of the issue, + merge request or comment in the email body instead. .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index 02efcecc889..fbe3ab912b6 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -1,5 +1,5 @@ - grouped_emojis = awardable.grouped_awards(with_thumbs: inline) -.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) } } +.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } } - awards_sort(grouped_emojis).each do |emoji, awards| %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", class: (award_active_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } } = emoji_icon(emoji, sprite: false) diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 53ed4fa991d..31db6ee0cad 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -23,7 +23,7 @@ .cover-desc.description = markdown(@group.description, pipeline: :description) -%div{ class: container_class } +%div.groups-header{ class: container_class } .top-area %ul.nav-links %li.active diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml index 61eff73da26..c2bcfb773a6 100644 --- a/app/views/projects/builds/_table.html.haml +++ b/app/views/projects/builds/_table.html.haml @@ -9,7 +9,7 @@ %thead %tr %th Status - %th Commit + %th Build - if admin %th Project %th Runner diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 494695a03a5..44e645a7e81 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -43,15 +43,16 @@ = icon("times-circle") Could not connect to the CI server. Please check your settings and try again. -- @merge_request.environments.each do |environment| - .mr-widget-heading - .ci_widget.ci-success - = ci_icon_for_status("success") - %span.hidden-sm - Deployed to - = succeed '.' do - = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment), class: 'environment' - - external_url = environment.external_url - - if external_url - = link_to external_url, target: '_blank' do - = icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true) +- @merge_request.environments.sort_by(&:name).each do |environment| + - if can?(current_user, :read_environment, environment) + .mr-widget-heading + .ci_widget.ci-success + = ci_icon_for_status("success") + %span.hidden-sm + Deployed to + = succeed '.' do + = link_to environment.name, environment_path(environment), class: 'environment' + - external_url = environment.external_url + - if external_url + = link_to external_url, target: '_blank' do + = icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true) diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index a5a5619fa12..4aa4ab46a2f 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -3,7 +3,7 @@ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New Snippet" do New Snippet - if can?(current_user, :update_project_snippet, @snippet) - = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-warning", title: 'Delete Snippet' do + = link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do Delete - if can?(current_user, :update_project_snippet, @snippet) = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped snippable-edit" do diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index b70fda88a79..9503dbded13 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -2,13 +2,16 @@ = render 'shared/snippets/header' -%article.file-holder.snippet-file-content - .file-title - = blob_icon 0, @snippet.file_name - = @snippet.file_name - .file-actions - = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") - = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" - = render 'shared/snippets/blob' - -%div#notes= render "projects/notes/notes_with_form" +.project-snippets + %article.file-holder.snippet-file-content + .file-title + = blob_icon 0, @snippet.file_name + = @snippet.file_name + .file-actions + = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") + = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank" + = render 'shared/snippets/blob' + + = render 'award_emoji/awards_block', awardable: @snippet, inline: true + + %div#notes= render "projects/notes/notes_with_form" diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index fa403da8f79..cd89155c616 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -10,3 +10,5 @@ = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']") = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank" = render 'shared/snippets/blob' + += render 'award_emoji/awards_block', awardable: @snippet, inline: true
\ No newline at end of file diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 9a052abe40a..2a57ac90bab 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -10,72 +10,72 @@ = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") .user-profile - .cover-block + .cover-block.user-cover-block .cover-controls - if @user == current_user = link_to profile_path, class: 'btn btn-gray' do = icon('pencil') - elsif current_user - %span.report-abuse - - if @user.abuse_report - %button.btn.btn-danger{ title: 'Already reported for abuse', - data: { toggle: 'tooltip', placement: 'left', container: 'body' }} - = icon('exclamation-circle') - - else - = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray', - title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do - = icon('exclamation-circle') + - if @user.abuse_report + %button.btn.btn-danger{ title: 'Already reported for abuse', + data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }} + = icon('exclamation-circle') + - else + = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray', + title: 'Report abuse', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('exclamation-circle') - if current_user - = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do = icon('rss') - if current_user.admin? - = link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('users') - .avatar-holder - = link_to avatar_icon(@user, 400), target: '_blank' do - = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: '' - .cover-title - = @user.name - - .cover-desc - %span.middle-dot-divider - @#{@user.username} - %span.middle-dot-divider - Member since #{@user.created_at.to_s(:medium)} + .profile-header + .avatar-holder + = link_to avatar_icon(@user, 400), target: '_blank' do + = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: '' + + .user-info + .cover-title + = @user.name + %span.handle + @#{@user.username} + + .cover-desc.member-date + %span.middle-dot-divider + Member since #{@user.created_at.to_s(:medium)} + + .cover-desc + - unless @user.public_email.blank? + .profile-link-holder.middle-dot-divider + = link_to @user.public_email, "mailto:#{@user.public_email}" + - unless @user.skype.blank? + .profile-link-holder.middle-dot-divider + = link_to "skype:#{@user.skype}", title: "Skype" do + = icon('skype') + - unless @user.linkedin.blank? + .profile-link-holder.middle-dot-divider + = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do + = icon('linkedin-square') + - unless @user.twitter.blank? + .profile-link-holder.middle-dot-divider + = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do + = icon('twitter-square') + - unless @user.website_url.blank? + .profile-link-holder.middle-dot-divider + = link_to @user.short_website_url, @user.full_website_url + - unless @user.location.blank? + .profile-link-holder.middle-dot-divider + = icon('map-marker') + = @user.location - if @user.bio.present? .cover-desc %p.profile-user-bio = @user.bio - .cover-desc - - unless @user.public_email.blank? - .profile-link-holder.middle-dot-divider - = link_to @user.public_email, "mailto:#{@user.public_email}" - - unless @user.skype.blank? - .profile-link-holder.middle-dot-divider - = link_to "skype:#{@user.skype}", title: "Skype" do - = icon('skype') - - unless @user.linkedin.blank? - .profile-link-holder.middle-dot-divider - = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do - = icon('linkedin-square') - - unless @user.twitter.blank? - .profile-link-holder.middle-dot-divider - = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do - = icon('twitter-square') - - unless @user.website_url.blank? - .profile-link-holder.middle-dot-divider - = link_to @user.short_website_url, @user.full_website_url - - unless @user.location.blank? - .profile-link-holder.middle-dot-divider - = icon('map-marker') - = @user.location - %ul.nav-links.center.user-profile-nav %li.js-activity-tab = link_to user_calendar_activities_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do diff --git a/config/application.rb b/config/application.rb index 8166b6003f6..4792f6670a8 100644 --- a/config/application.rb +++ b/config/application.rb @@ -116,10 +116,6 @@ module Gitlab redis_config_hash = Gitlab::Redis.params redis_config_hash[:namespace] = Gitlab::Redis::CACHE_NAMESPACE redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever - if Sidekiq.server? # threaded context - redis_config_hash[:pool_size] = Sidekiq.options[:concurrency] + 5 - redis_config_hash[:pool_timeout] = 1 - end config.cache_store = :redis_store, redis_config_hash config.active_record.raise_in_transactional_callbacks = true diff --git a/config/routes.rb b/config/routes.rb index c4eee59e7aa..4d6ec699cbd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -35,6 +35,10 @@ Rails.application.routes.draw do post :approve_access_request, on: :member end + concern :awardable do + post :toggle_award_emoji, on: :member + end + namespace :ci do # CI API Ci::API::API.logger Rails.logger @@ -98,7 +102,7 @@ Rails.application.routes.draw do # # Global snippets # - resources :snippets do + resources :snippets, concerns: :awardable do member do get 'raw' end @@ -110,7 +114,6 @@ Rails.application.routes.draw do # # Invites # - resources :invites, only: [:show], constraints: { id: /[A-Za-z0-9_-]+/ } do member do post :accept @@ -662,7 +665,7 @@ Rails.application.routes.draw do end end - resources :snippets, constraints: { id: /\d+/ } do + resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do member do get 'raw' end @@ -724,7 +727,7 @@ Rails.application.routes.draw do end end - resources :merge_requests, constraints: { id: /\d+/ } do + resources :merge_requests, concerns: :awardable, constraints: { id: /\d+/ } do member do get :commits get :diffs @@ -736,7 +739,6 @@ Rails.application.routes.draw do post :cancel_merge_when_build_succeeds get :ci_status post :toggle_subscription - post :toggle_award_emoji post :remove_wip get :diff_for_path post :resolve_conflicts @@ -840,10 +842,9 @@ Rails.application.routes.draw do end end - resources :issues, constraints: { id: /\d+/ } do + resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do member do post :toggle_subscription - post :toggle_award_emoji post :mark_as_spam get :referenced_merge_requests get :related_branches @@ -871,9 +872,8 @@ Rails.application.routes.draw do resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ } - resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do + resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do member do - post :toggle_award_emoji delete :delete_attachment post :resolve delete :resolve, action: :unresolve diff --git a/doc/README.md b/doc/README.md index 254394eb63e..dd0eb97489e 100644 --- a/doc/README.md +++ b/doc/README.md @@ -29,9 +29,9 @@ - [Install](install/README.md) Requirements, directory structures and installation from source. - [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components. - [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter. -- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages. +- [Issue closing pattern](administration/issue_closing_pattern.md) Customize how to close an issue from commit messages. - [Koding](administration/integration/koding.md) Set up Koding to use with GitLab. -- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars. +- [Libravatar](customization/libravatar.md) Use Libravatar instead of Gravatar for user avatars. - [Log system](administration/logs.md) Log system. - [Environment Variables](administration/environment_variables.md) to configure GitLab. - [Operations](operations/README.md) Keeping GitLab up and running. diff --git a/doc/administration/issue_closing_pattern.md b/doc/administration/issue_closing_pattern.md new file mode 100644 index 00000000000..28e1fd4e12e --- /dev/null +++ b/doc/administration/issue_closing_pattern.md @@ -0,0 +1,49 @@ +# Issue closing pattern + +>**Note:** +This is the administration documentation. +There is a separate [user documentation] on issue closing pattern. + +When a commit or merge request resolves one or more issues, it is possible to +automatically have these issues closed when the commit or merge request lands +in the project's default branch. + +## Change the issue closing pattern + +In order to change the pattern you need to have access to the server that GitLab +is installed on. + +The default pattern can be located in [gitlab.yml.example] under the +"Automatic issue closing" section. + +> **Tip:** +You are advised to use http://rubular.com to test the issue closing pattern. +Because Rubular doesn't understand `%{issue_ref}`, you can replace this by +`#\d+` when testing your patterns, which matches only local issue references like `#123`. + +**For Omnibus installations** + +1. Open `/etc/gitlab/gitlab.rb` with your editor. +1. Change the value of `gitlab_rails['issue_closing_pattern']` to a regular + expression of your liking: + + ```ruby + gitlab_rails['issue_closing_pattern'] = "((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)" + ``` +1. [Reconfigure] GitLab for the changes to take effect. + +**For installations from source** + +1. Open `gitlab.yml` with your editor. +1. Change the value of `issue_closing_pattern`: + + ```yaml + issue_closing_pattern: "((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)" + ``` + +1. [Restart] GitLab for the changes to take effect. + +[gitlab.yml.example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example +[reconfigure]: restart_gitlab.md#omnibus-gitlab-reconfigure +[restart]: restart_gitlab.md#installations-from-source +[user documentation]: ../user/project/issues/automatic_issue_closing.md diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md index 72ec99b7c56..c464e3f3f71 100644 --- a/doc/api/award_emoji.md +++ b/doc/api/award_emoji.md @@ -1,12 +1,13 @@ # Award Emoji -> [Introduced][ce-4575] in GitLab 8.9. +> [Introduced][ce-4575] in GitLab 8.9, Snippet support in 8.12 + An awarded emoji tells a thousand words, and can be awarded on issues, merge -requests and notes/comments. Issues, merge requests and notes are further called +requests, snippets, and notes/comments. Issues, merge requests, snippets, and notes are further called `awardables`. -## Issues and merge requests +## Issues, merge requests, and snippets ### List an awardable's award emoji @@ -15,6 +16,7 @@ Gets a list of all award emoji ``` GET /projects/:id/issues/:issue_id/award_emoji GET /projects/:id/merge_requests/:merge_request_id/award_emoji +GET /projects/:id/snippets/:snippet_id/award_emoji ``` Parameters: @@ -69,11 +71,12 @@ Example Response: ### Get single award emoji -Gets a single award emoji from an issue or merge request. +Gets a single award emoji from an issue, snippet, or merge request. ``` GET /projects/:id/issues/:issue_id/award_emoji/:award_id GET /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id +GET /projects/:id/snippets/:snippet_id/award_emoji/:award_id ``` Parameters: @@ -116,6 +119,7 @@ This end point creates an award emoji on the specified resource ``` POST /projects/:id/issues/:issue_id/award_emoji POST /projects/:id/merge_requests/:merge_request_id/award_emoji +POST /projects/:id/snippets/:snippet_id/award_emoji ``` Parameters: @@ -159,6 +163,7 @@ admins or the author of the award. Status code 200 on success, 401 if unauthoriz ``` DELETE /projects/:id/issues/:issue_id/award_emoji/:award_id DELETE /projects/:id/merge_requests/:merge_request_id/award_emoji/:award_id +DELETE /projects/:id/snippets/:snippet_id/award_emoji/:award_id ``` Parameters: @@ -197,7 +202,7 @@ Example Response: ## Award Emoji on Notes The endpoints documented above are available for Notes as well. Notes -are a sub-resource of Issues and Merge Requests. The examples below +are a sub-resource of Issues, Merge Requests, or Snippets. The examples below describe working with Award Emoji on notes for an Issue, but can be easily adapted for notes on a Merge Request. diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md index 4620bb2dcde..31164ccd465 100644 --- a/doc/customization/issue_closing.md +++ b/doc/customization/issue_closing.md @@ -1,39 +1,4 @@ -# Issue closing pattern +This document was split into: -When a commit or merge request resolves one or more issues, it is possible to automatically have these issues closed when the commit or merge request lands in the project's default branch. - -If a commit message or merge request description contains a sentence matching the regular expression below, all issues referenced from -the matched text will be closed. This happens when the commit is pushed to a project's default branch, or when a commit or merge request is merged into there. - -When not specified, the default `issue_closing_pattern` as shown below will be used: - -```bash -((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+) -``` - -Here, `%{issue_ref}` is a complex regular expression defined inside GitLab, that matches a reference to a local issue (`#123`), cross-project issue (`group/project#123`) or a link to an issue (`https://gitlab.example.com/group/project/issues/123`). - -For example: - -``` -git commit -m "Awesome commit message (Fix #20, Fixes #21 and Closes group/otherproject#22). This commit is also related to #17 and fixes #18, #19 and https://gitlab.example.com/group/otherproject/issues/23." -``` - -will close `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed to, as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as it does not match the pattern. It also works with multiline commit messages. - -Tip: you can test this closing pattern at [http://rubular.com][1]. Use this site -to test your own patterns. -Because Rubular doesn't understand `%{issue_ref}`, you can replace this by `#\d+` in testing, which matches only local issue references like `#123`. - -## Change the pattern - -For Omnibus installs you can change the default pattern in `/etc/gitlab/gitlab.rb`: - -``` -issue_closing_pattern: '((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)' -``` - -For manual installs you can customize the pattern in [gitlab.yml][0] using the `issue_closing_pattern` key. - -[0]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example -[1]: http://rubular.com/r/Xmbexed1OJ +- [administration/issue_closing_pattern.md](../administration/issue_closing_pattern.md). +- [user/project/issues/automatic_issue_closing](../user/project/issues/automatic_issue_closing.md). diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md index 5221d85b661..da9a165b8f5 100644 --- a/doc/gitlab-basics/create-issue.md +++ b/doc/gitlab-basics/create-issue.md @@ -1,6 +1,6 @@ # How to create an Issue in GitLab -The Issue Tracker is a good place to add things that need to be improved or solved in a project. +The Issue Tracker is a good place to add things that need to be improved or solved in a project. To create an Issue, sign in to GitLab. @@ -24,4 +24,4 @@ You may assign the Issue to a user, add a milestone and add labels (they are all  -Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](http://docs.gitlab.com/ce/customization/issue_closing.html). +Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](../user/project/issues/automatic_issue_closing.md). diff --git a/doc/intro/README.md b/doc/intro/README.md index 71fef50ceb4..1790b2b761f 100644 --- a/doc/intro/README.md +++ b/doc/intro/README.md @@ -22,7 +22,7 @@ Create merge requests and review code. - [Fork a project and contribute to it](../workflow/forking_workflow.md) - [Create a new merge request](../gitlab-basics/add-merge-request.md) -- [Automatically close issues from merge requests](../customization/issue_closing.md) +- [Automatically close issues from merge requests](../user/project/issues/automatic_issue_closing.md) - [Automatically merge when your builds succeed](../user/project/merge_requests/merge_when_build_succeeds.md) - [Revert any commit](../user/project/merge_requests/revert_changes.md) - [Cherry-pick any commit](../user/project/merge_requests/cherry_pick_changes.md) diff --git a/doc/user/project/issues/automatic_issue_closing.md b/doc/user/project/issues/automatic_issue_closing.md new file mode 100644 index 00000000000..d6f3a7d5555 --- /dev/null +++ b/doc/user/project/issues/automatic_issue_closing.md @@ -0,0 +1,55 @@ +# Automatic issue closing + +>**Note:** +This is the user docs. In order to change the default issue closing pattern, +follow the steps in the [administration docs]. + +When a commit or merge request resolves one or more issues, it is possible to +automatically have these issues closed when the commit or merge request lands +in the project's default branch. + +If a commit message or merge request description contains a sentence matching +a certain regular expression, all issues referenced from the matched text will +be closed. This happens when the commit is pushed to a project's **default** +branch, or when a commit or merge request is merged into it. + +## Default closing pattern value + +When not specified, the default issue closing pattern as shown below will be +used: + +```bash +((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing))(:?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z][A-Z0-9_]+-\d+))+) +``` + +Note that `%{issue_ref}` is a complex regular expression defined inside GitLab's +source code that can match a reference to 1) a local issue (`#123`), +2) a cross-project issue (`group/project#123`) or 3) a link to an issue +(`https://gitlab.example.com/group/project/issues/123`). + +--- + +This translates to the following keywords: + +- Close, Closes, Closed, Closing, close, closes, closed, closing +- Fix, Fixes, Fixed, Fixing, fix, fixes, fixed, fixing +- Resolve, Resolves, Resolved, Resolving, resolve, resolves, resolved, resolving + +--- + +For example the following commit message: + +``` +Awesome commit message + +Fix #20, Fixes #21 and Closes group/otherproject#22. +This commit is also related to #17 and fixes #18, #19 +and https://gitlab.example.com/group/otherproject/issues/23. +``` + +will close `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed +to, as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as +it does not match the pattern. It works with multi-line commit messages as well +as one-liners when used with `git commit -m`. + +[administration docs]: ../../../administration/issue_closing_pattern.md diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md index 7c041d019bb..993c6bfb7e9 100644 --- a/doc/user/project/repository/web_editor.md +++ b/doc/user/project/repository/web_editor.md @@ -172,4 +172,4 @@ you commit the changes you will be taken to a new merge request form.  [ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808 -[issue closing pattern]: ../customization/issue_closing.md +[issue closing pattern]: ../user/project/issues/automatic_issue_closing.md diff --git a/doc/workflow/README.md b/doc/workflow/README.md index e8ecb8d8fb4..2d9bfbc0629 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -1,5 +1,6 @@ # Workflow +- [Automatic issue closing](../user/project/issues/automatic_issue_closing.md) - [Change your time zone](timezone.md) - [Cycle Analytics](../user/project/cycle_analytics.md) - [Description templates](../user/project/description_templates.md) diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb index 7c22b17e4e5..2461a783ea8 100644 --- a/lib/api/award_emoji.rb +++ b/lib/api/award_emoji.rb @@ -1,12 +1,12 @@ module API class AwardEmoji < Grape::API before { authenticate! } - AWARDABLES = [Issue, MergeRequest] + AWARDABLES = %w[issue merge_request snippet] resource :projects do AWARDABLES.each do |awardable_type| - awardable_string = awardable_type.to_s.underscore.pluralize - awardable_id_string = "#{awardable_type.to_s.underscore}_id" + awardable_string = awardable_type.pluralize + awardable_id_string = "#{awardable_type}_id" [ ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji", ":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji" @@ -87,9 +87,7 @@ module API helpers do def can_read_awardable? - ability = "read_#{awardable.class.to_s.underscore}".to_sym - - can?(current_user, ability, awardable) + can?(current_user, read_ability(awardable), awardable) end def can_award_awardable? @@ -100,18 +98,25 @@ module API @awardable ||= begin if params.include?(:note_id) - noteable.notes.find(params[:note_id]) + note_id = params.delete(:note_id) + + awardable.notes.find(note_id) + elsif params.include?(:issue_id) + user_project.issues.find(params[:issue_id]) + elsif params.include?(:merge_request_id) + user_project.merge_requests.find(params[:merge_request_id]) else - noteable + user_project.snippets.find(params[:snippet_id]) end end end - def noteable - if params.include?(:issue_id) - user_project.issues.find(params[:issue_id]) + def read_ability(awardable) + case awardable + when Note + read_ability(awardable.noteable) else - user_project.merge_requests.find(params[:merge_request_id]) + :"read_#{awardable.class.to_s.underscore}" end end end diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index 3ab99360206..3cd515e4a3a 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -19,6 +19,8 @@ module Gitlab end def committer_hash(email:, name:) + return if email.nil? || name.nil? + { email: email, name: name, diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 925a952156f..88803d76623 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -10,6 +10,7 @@ project_tree: - milestone: - :events - snippets: + - :award_emoji - notes: :author - :releases @@ -66,6 +67,8 @@ excluded_attributes: - :milestone_id merge_requests: - :milestone_id + award_emoji: + - :awardable_id methods: statuses: diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index 2a89159c070..41d263a46a4 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' describe SnippetsController do - describe 'GET #show' do - let(:user) { create(:user) } + let(:user) { create(:user) } + describe 'GET #show' do context 'when the personal snippet is private' do let(:personal_snippet) { create(:personal_snippet, :private, author: user) } @@ -230,4 +230,33 @@ describe SnippetsController do end end end + + context 'award emoji on snippets' do + let(:personal_snippet) { create(:personal_snippet, :public, author: user) } + let(:another_user) { create(:user) } + + before do + sign_in(another_user) + end + + describe 'POST #toggle_award_emoji' do + it "toggles the award emoji" do + expect do + post(:toggle_award_emoji, id: personal_snippet.to_param, name: "thumbsup") + end.to change { personal_snippet.award_emoji.count }.from(0).to(1) + + expect(response.status).to eq(200) + end + + it "removes the already awarded emoji" do + post(:toggle_award_emoji, id: personal_snippet.to_param, name: "thumbsup") + + expect do + post(:toggle_award_emoji, id: personal_snippet.to_param, name: "thumbsup") + end.to change { personal_snippet.award_emoji.count }.from(1).to(0) + + expect(response.status).to eq(200) + end + end + end end diff --git a/spec/factories/group_members.rb b/spec/factories/group_members.rb index 2044ebec09a..795df5dfda9 100644 --- a/spec/factories/group_members.rb +++ b/spec/factories/group_members.rb @@ -3,5 +3,11 @@ FactoryGirl.define do access_level { GroupMember::OWNER } group user + + trait(:guest) { access_level GroupMember::GUEST } + trait(:reporter) { access_level GroupMember::REPORTER } + trait(:developer) { access_level GroupMember::DEVELOPER } + trait(:master) { access_level GroupMember::MASTER } + trait(:owner) { access_level GroupMember::OWNER } end end diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb index 7e2c701e401..27c986c5187 100644 --- a/spec/features/projects/import_export/export_file_spec.rb +++ b/spec/features/projects/import_export/export_file_spec.rb @@ -15,7 +15,7 @@ feature 'Import/Export - project export integration test', feature: true, js: tr let(:sensitive_words) { %w[pass secret token key] } let(:safe_list) do { - token: [ProjectHook, Ci::Trigger], + token: [ProjectHook, Ci::Trigger, CommitStatus], key: [Project, Ci::Variable, :yaml_variables] } end diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb new file mode 100644 index 00000000000..219198eff60 --- /dev/null +++ b/spec/lib/gitlab/git_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe Gitlab::Git, lib: true do + let(:committer_email) { FFaker::Internet.email } + + # I have to remove periods from the end of the name + # This happened when the user's name had a suffix (i.e. "Sr.") + # This seems to be what git does under the hood. For example, this commit: + # + # $ git commit --author='Foo Sr. <foo@example.com>' -m 'Where's my trailing period?' + # + # results in this: + # + # $ git show --pretty + # ... + # Author: Foo Sr <foo@example.com> + # ... + let(:committer_name) { FFaker::Name.name.chomp("\.") } + + describe 'committer_hash' do + it "returns a hash containing the given email and name" do + committer_hash = Gitlab::Git::committer_hash(email: committer_email, name: committer_name) + + expect(committer_hash[:email]).to eq(committer_email) + expect(committer_hash[:name]).to eq(committer_name) + expect(committer_hash[:time]).to be_a(Time) + end + + context 'when email is nil' do + it "returns nil" do + committer_hash = Gitlab::Git::committer_hash(email: nil, name: committer_name) + + expect(committer_hash).to be_nil + end + end + + context 'when name is nil' do + it "returns nil" do + committer_hash = Gitlab::Git::committer_hash(email: committer_email, name: nil) + + expect(committer_hash).to be_nil + end + end + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 30968ba2d5f..006569254a6 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -13,6 +13,8 @@ issues: - user_agent_detail - moved_to - events +- merge_requests_closing_issues +- metrics events: - author - project @@ -47,6 +49,7 @@ snippets: - author - project - notes +- award_emoji releases: - project project_members: @@ -71,6 +74,8 @@ merge_requests: - merge_request_diffs - merge_request_diff - events +- merge_requests_closing_issues +- metrics merge_request_diff: - merge_request pipelines: @@ -101,6 +106,10 @@ protected_branches: - project - merge_access_levels - push_access_levels +merge_access_levels: +- protected_branch +push_access_levels: +- protected_branch project: - taggings - base_tags @@ -172,4 +181,7 @@ project: - triggers - environments - deployments -- project_feature
\ No newline at end of file +- project_feature +award_emoji: +- awardable +- user
\ No newline at end of file diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index f2d272ca7e2..8bccd313d6c 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -214,6 +214,7 @@ CommitStatus: - when - yaml_variables - queued_at +- token Ci::Variable: - id - project_id @@ -307,4 +308,23 @@ ProjectFeature: - snippets_access_level - builds_access_level - created_at -- updated_at
\ No newline at end of file +- updated_at +ProtectedBranch::MergeAccessLevel: +- id +- protected_branch_id +- access_level +- created_at +- updated_at +ProtectedBranch::PushAccessLevel: +- id +- protected_branch_id +- access_level +- created_at +- updated_at +AwardEmoji: +- id +- user_id +- name +- awardable_type +- created_at +- updated_at diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 06feeb1bbba..433aba7747b 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -495,15 +495,62 @@ describe MergeRequest, models: true do end describe '#all_pipelines' do - let!(:pipelines) do - subject.merge_request_diff.commits.map do |commit| - create(:ci_empty_pipeline, project: subject.source_project, sha: commit.id, ref: subject.source_branch) + shared_examples 'returning pipelines with proper ordering' do + let!(:all_pipelines) do + subject.all_commits_sha.map do |sha| + create(:ci_empty_pipeline, + project: subject.source_project, + sha: sha, + ref: subject.source_branch) + end + end + + it 'returns all pipelines' do + expect(subject.all_pipelines).not_to be_empty + expect(subject.all_pipelines).to eq(all_pipelines.reverse) + end + end + + context 'with single merge_request_diffs' do + it_behaves_like 'returning pipelines with proper ordering' + end + + context 'with multiple irrelevant merge_request_diffs' do + before do + subject.update(target_branch: 'markdown') + end + + it_behaves_like 'returning pipelines with proper ordering' + end + + context 'with unsaved merge request' do + subject { build(:merge_request) } + + let!(:pipeline) do + create(:ci_empty_pipeline, + project: subject.project, + sha: subject.diff_head_sha, + ref: subject.source_branch) + end + + it 'returns pipelines from diff_head_sha' do + expect(subject.all_pipelines).to contain_exactly(pipeline) end end + end + + describe '#all_commits_sha' do + let(:all_commits_sha) do + subject.merge_request_diffs.flat_map(&:commits).map(&:sha).uniq + end + + before do + subject.update(target_branch: 'markdown') + end - it 'returns a pipelines from source projects with proper ordering' do - expect(subject.all_pipelines).not_to be_empty - expect(subject.all_pipelines).to eq(pipelines.reverse) + it 'returns all SHA from all merge_request_diffs' do + expect(subject.merge_request_diffs.size).to eq(2) + expect(subject.all_commits_sha).to eq(all_commits_sha) end end @@ -701,16 +748,57 @@ describe MergeRequest, models: true do end end - describe "#environments" do + describe '#environments' do let(:project) { create(:project) } let(:merge_request) { create(:merge_request, source_project: project) } - it 'selects deployed environments' do - environments = create_list(:environment, 3, project: project) - create(:deployment, environment: environments.first, sha: project.commit('master').id) - create(:deployment, environment: environments.second, sha: project.commit('feature').id) + context 'with multiple environments' do + let(:environments) { create_list(:environment, 3, project: project) } - expect(merge_request.environments).to eq [environments.first] + before do + create(:deployment, environment: environments.first, ref: 'master', sha: project.commit('master').id) + create(:deployment, environment: environments.second, ref: 'feature', sha: project.commit('feature').id) + end + + it 'selects deployed environments' do + expect(merge_request.environments).to contain_exactly(environments.first) + end + end + + context 'with environments on source project' do + let(:source_project) do + create(:project) do |fork_project| + fork_project.create_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) + end + end + + let(:merge_request) do + create(:merge_request, + source_project: source_project, source_branch: 'feature', + target_project: project) + end + + let(:source_environment) { create(:environment, project: source_project) } + + before do + create(:deployment, environment: source_environment, ref: 'feature', sha: merge_request.diff_head_sha) + end + + it 'selects deployed environments' do + expect(merge_request.environments).to contain_exactly(source_environment) + end + + context 'with environments on target project' do + let(:target_environment) { create(:environment, project: project) } + + before do + create(:deployment, environment: target_environment, tag: true, sha: merge_request.diff_head_sha) + end + + it 'selects deployed environments' do + expect(merge_request.environments).to contain_exactly(source_environment, target_environment) + end + end end context 'without a diff_head_commit' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index a388ff703a6..83f61f0af0a 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1647,6 +1647,47 @@ describe Project, models: true do end end + describe '#environments_for' do + let(:project) { create(:project) } + let(:environment) { create(:environment, project: project) } + + context 'tagged deployment' do + before do + create(:deployment, environment: environment, ref: '1.0', tag: true, sha: project.commit.id) + end + + it 'returns environment when with_tags is set' do + expect(project.environments_for('master', project.commit, with_tags: true)).to contain_exactly(environment) + end + + it 'does not return environment when no with_tags is set' do + expect(project.environments_for('master', project.commit)).to be_empty + end + + it 'does not return environment when commit is not part of deployment' do + expect(project.environments_for('master', project.commit('feature'))).to be_empty + end + end + + context 'branch deployment' do + before do + create(:deployment, environment: environment, ref: 'master', sha: project.commit.id) + end + + it 'returns environment when ref is set' do + expect(project.environments_for('master', project.commit)).to contain_exactly(environment) + end + + it 'does not environment when ref is different' do + expect(project.environments_for('feature', project.commit)).to be_empty + end + + it 'does not return environment when commit is not part of deployment' do + expect(project.environments_for('master', project.commit('feature'))).to be_empty + end + end + end + def enable_lfs allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) end diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 5eaf0d3b7a6..f979d66c88c 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -73,6 +73,68 @@ describe ProjectTeam, models: true do end end + describe '#fetch_members' do + context 'personal project' do + let(:project) { create(:empty_project) } + + it 'returns project members' do + user = create(:user) + project.team << [user, :guest] + + expect(project.team.members).to contain_exactly(user) + end + + it 'returns project members of a specified level' do + user = create(:user) + project.team << [user, :reporter] + + expect(project.team.guests).to be_empty + expect(project.team.reporters).to contain_exactly(user) + end + + it 'returns invited members of a group' do + group_member = create(:group_member) + + project.project_group_links.create!( + group: group_member.group, + group_access: Gitlab::Access::GUEST + ) + + expect(project.team.members).to contain_exactly(group_member.user) + end + + it 'returns invited members of a group of a specified level' do + group_member = create(:group_member) + + project.project_group_links.create!( + group: group_member.group, + group_access: Gitlab::Access::REPORTER + ) + + expect(project.team.guests).to be_empty + expect(project.team.reporters).to contain_exactly(group_member.user) + end + end + + context 'group project' do + let(:group) { create(:group) } + let(:project) { create(:empty_project, group: group) } + + it 'returns project members' do + group_member = create(:group_member, group: group) + + expect(project.team.members).to contain_exactly(group_member.user) + end + + it 'returns project members of a specified level' do + group_member = create(:group_member, :reporter, group: group) + + expect(project.team.guests).to be_empty + expect(project.team.reporters).to contain_exactly(group_member.user) + end + end + end + describe '#find_member' do context 'personal project' do let(:project) { create(:empty_project) } diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index 0621c6a06ce..e6bc5296398 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -9,12 +9,14 @@ describe Snippet, models: true do it { is_expected.to include_module(Participable) } it { is_expected.to include_module(Referable) } it { is_expected.to include_module(Sortable) } + it { is_expected.to include_module(Awardable) } end describe 'associations' do it { is_expected.to belong_to(:author).class_name('User') } it { is_expected.to belong_to(:project) } it { is_expected.to have_many(:notes).dependent(:destroy) } + it { is_expected.to have_many(:award_emoji).dependent(:destroy) } end describe 'validation' do diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index 981a6791881..5ad4fc4865a 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } - let!(:project) { create(:project) } + let!(:project) { create(:empty_project) } let(:issue) { create(:issue, project: project) } let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) } let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } @@ -39,6 +39,19 @@ describe API::API, api: true do end end + context 'on a snippet' do + let(:snippet) { create(:project_snippet, :public, project: project) } + let!(:award) { create(:award_emoji, awardable: snippet) } + + it 'returns the awarded emoji' do + get api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(award.name) + end + end + context 'when the user has no access' do it 'returns a status code 404' do user1 = create(:user) @@ -91,6 +104,20 @@ describe API::API, api: true do end end + context 'on a snippet' do + let(:snippet) { create(:project_snippet, :public, project: project) } + let!(:award) { create(:award_emoji, awardable: snippet) } + + it 'returns the awarded emoji' do + get api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(award.name) + expect(json_response['awardable_id']).to eq(snippet.id) + expect(json_response['awardable_type']).to eq("Snippet") + end + end + context 'when the user has no access' do it 'returns a status code 404' do user1 = create(:user) @@ -160,6 +187,18 @@ describe API::API, api: true do end end end + + context 'on a snippet' do + it 'creates a new award emoji' do + snippet = create(:project_snippet, :public, project: project) + + post api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji", user), name: 'blowfish' + + expect(response).to have_http_status(201) + expect(json_response['name']).to eq('blowfish') + expect(json_response['user']['username']).to eq(user.username) + end + end end describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do @@ -229,6 +268,19 @@ describe API::API, api: true do expect(response).to have_http_status(404) end end + + context 'when the awardable is a Snippet' do + let(:snippet) { create(:project_snippet, :public, project: project) } + let!(:award) { create(:award_emoji, awardable: snippet, user: user) } + + it 'deletes the award' do + expect do + delete api("/projects/#{project.id}/snippets/#{snippet.id}/award_emoji/#{award.id}", user) + end.to change { snippet.award_emoji.count }.from(1).to(0) + + expect(response).to have_http_status(200) + end + end end describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_emoji_id' do diff --git a/spec/views/projects/merge_requests/_heading.html.haml_spec.rb b/spec/views/projects/merge_requests/_heading.html.haml_spec.rb index 733b2dfa7ff..21f49d396e7 100644 --- a/spec/views/projects/merge_requests/_heading.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/_heading.html.haml_spec.rb @@ -15,6 +15,8 @@ describe 'projects/merge_requests/widget/_heading' do assign(:merge_request, merge_request) assign(:project, project) + allow(view).to receive(:can?).and_return(true) + render end |