diff options
67 files changed, 641 insertions, 314 deletions
diff --git a/CHANGELOG b/CHANGELOG index 74217e80bfe..d3171da81c3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.6.0 (unreleased) + - Fix bug where wrong commit ID was being used in a merge request diff to show old image (Stan Hu) - Add confidential issues - Bump gitlab_git to 9.0.3 (Stan Hu) - Support Golang subpackage fetching (Stan Hu) @@ -8,6 +9,8 @@ v 8.6.0 (unreleased) - New branch button appears on issues where applicable - Contributions to forked projects are included in calendar - Improve the formatting for the user page bio (Connor Shea) + - Easily (un)mark merge request as WIP using link + - Use specialized system notes when MR is (un)marked as WIP - Removed the default password from the initial admin account created during setup. A password can be provided during setup (see installation docs), or GitLab will ask the user to create a new one upon first visit. @@ -20,6 +23,7 @@ v 8.6.0 (unreleased) - Memoize @group in Admin::GroupsController (Yatish Mehta) - Indicate how much an MR diverged from the target branch (Pierre de La Morinerie) - Added omniauth-auth0 Gem (Daniel Carraro) + - Add label description in tooltip to labels in issue index and sidebar - Strip leading and trailing spaces in URL validator (evuez) - Add "last_sign_in_at" and "confirmed_at" to GET /users/* API endpoints for admins (evuez) - Return empty array instead of 404 when commit has no statuses in commit status API @@ -43,6 +47,7 @@ v 8.6.0 (unreleased) - Increase the notes polling timeout over time (Roberto Dip) - Add shortcut to toggle markdown preview (Florent Baldino) - Show labels in dashboard and group milestone views + - Fix an issue when the target branch of a MR had been deleted - Add main language of a project in the list of projects (Tiago Botelho) - Add #upcoming filter to Milestone filter (Tiago Botelho) - Add ability to show archived projects on dashboard, explore and group pages @@ -51,6 +56,7 @@ v 8.6.0 (unreleased) - Continue parameters are checked to ensure redirection goes to the same instance - User deletion is now done in the background so the request can not time out - Canceled builds are now ignored in compound build status if marked as `allowed to fail` + - Trigger a todo for mentions on commits page v 8.5.8 - Bump Git version requirement to 2.7.4 diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index c81e8bf760a..960585245d7 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -167,7 +167,11 @@ class GitLabDropdown hidden: => if @options.filterable - @dropdown.find(".dropdown-input-field").blur().val("") + @dropdown + .find(".dropdown-input-field") + .blur() + .val("") + .trigger("keyup") if @dropdown.find(".dropdown-toggle-page").length $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS diff --git a/app/assets/javascripts/issuable_form.js.coffee b/app/assets/javascripts/issuable_form.js.coffee index 48c249943f2..6c1699c178c 100644 --- a/app/assets/javascripts/issuable_form.js.coffee +++ b/app/assets/javascripts/issuable_form.js.coffee @@ -1,4 +1,5 @@ class @IssuableForm + wipRegex: /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i constructor: (@form) -> GitLab.GfmAutoComplete.setup() new UsersSelect() @@ -14,6 +15,8 @@ class @IssuableForm @form.on "submit", @resetAutosave @form.on "click", ".btn-cancel", @resetAutosave + @initWip() + initAutosave: -> new Autosave @titleField, [ document.location.pathname, @@ -30,3 +33,41 @@ class @IssuableForm resetAutosave: => @titleField.data("autosave").reset() @descriptionField.data("autosave").reset() + + initWip: -> + @$wipExplanation = @form.find(".js-wip-explanation") + @$noWipExplanation = @form.find(".js-no-wip-explanation") + return unless @$wipExplanation.length and @$noWipExplanation.length + + @form.on "click", ".js-toggle-wip", @toggleWip + + @titleField.on "keyup blur", @renderWipExplanation + + @renderWipExplanation() + + workInProgress: -> + @wipRegex.test @titleField.val() + + renderWipExplanation: => + if @workInProgress() + @$wipExplanation.show() + @$noWipExplanation.hide() + else + @$wipExplanation.hide() + @$noWipExplanation.show() + + toggleWip: (event) => + event.preventDefault() + + if @workInProgress() + @removeWip() + else + @addWip() + + @renderWipExplanation() + + removeWip: -> + @titleField.val @titleField.val().replace(@wipRegex, "") + + addWip: -> + @titleField.val "WIP: #{@titleField.val()}" diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index e6c1446f14f..4a0c18a99a6 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -11,7 +11,7 @@ class @LabelsSelect newColorField = $('#new_label_color') showNo = $dropdown.data('show-no') showAny = $dropdown.data('show-any') - defaultLabel = $dropdown.text().trim() + defaultLabel = $dropdown.data('default-label') if newLabelField.length $('.suggest-colors-dropdown a').on 'click', (e) -> @@ -39,18 +39,9 @@ class @LabelsSelect $dropdown.glDropdown( data: (term, callback) -> - # We have to fetch the JS version of the labels list because there is no - # public facing JSON url for labels $.ajax( url: labelUrl ).done (data) -> - html = $(data) - data = [] - html.find('.label-row a').each -> - data.push( - title: $(@).text().trim() - ) - if showNo data.unshift( id: 0 diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 8322b4c46ad..839e6ec2c08 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -3,6 +3,8 @@ # Handles persisting and restoring the current tab selection and lazily-loading # content on the MergeRequests#show page. # +#= require jquery.cookie +# # ### Example Markup # # <ul class="nav-links merge-request-tabs"> @@ -68,11 +70,15 @@ class @MergeRequestTabs if action == 'commits' @loadCommits($target.attr('href')) + @expandView() else if action == 'diffs' @loadDiff($target.attr('href')) @shrinkView() else if action == 'builds' @loadBuilds($target.attr('href')) + @expandView() + else + @expandView() @setCurrentAction(action) @@ -189,11 +195,24 @@ class @MergeRequestTabs $('.container-fluid').removeClass('container-limited') shrinkView: -> - $gutterIcon = $('.js-sidebar-toggle i') + $gutterIcon = $('.js-sidebar-toggle i:visible') # Wait until listeners are set setTimeout( -> - # Only when sidebar is collapsed + # Only when sidebar is expanded if $gutterIcon.is('.fa-angle-double-right') - $gutterIcon.closest('a').trigger('click',[true]) + $gutterIcon.closest('a').trigger('click', [true]) + , 0) + + # Expand the issuable sidebar unless the user explicitly collapsed it + expandView: -> + return if $.cookie('collapsed_gutter') == 'true' + + $gutterIcon = $('.js-sidebar-toggle i:visible') + + # Wait until listeners are set + setTimeout( -> + # Only when sidebar is collapsed + if $gutterIcon.is('.fa-angle-double-left') + $gutterIcon.closest('a').trigger('click', [true]) , 0) diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index 0287d98b1ec..e17a1adb648 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -8,22 +8,13 @@ class @MilestoneSelect showNo = $dropdown.data('show-no') showAny = $dropdown.data('show-any') useId = $dropdown.data('use-id') - defaultLabel = $dropdown.text().trim() + defaultLabel = $dropdown.data('default-label') $dropdown.glDropdown( data: (term, callback) -> $.ajax( url: milestonesUrl ).done (data) -> - html = $(data) - data = [] - html.find('.milestone strong a').each -> - link = $(@).attr('href').split('/') - data.push( - id: link[link.length - 1] - title: $(@).text().trim() - ) - if showNo data.unshift( id: '0' diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index 48831dd6bc4..3d6452d2f46 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -11,7 +11,7 @@ class @UsersSelect showAnyUser = $dropdown.data('any-user') firstUser = $dropdown.data('first-user') selectedId = $dropdown.data('selected') - defaultLabel = $dropdown.text().trim() + defaultLabel = $dropdown.data('default-label') $dropdown.glDropdown( data: (term, callback) => diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 2b4bb1eebf9..b17c8bcbb1e 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -111,14 +111,17 @@ ul.content-list { > li { border-color: $table-border-color; - color: $list-text-color; font-size: $list-font-size; + color: $list-text-color; .title { - color: $list-title-color; font-weight: 600; } + a { + color: $gl-dark-link-color; + } + .description { p { @include str-truncated; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 211ead7319d..be626678bd7 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -1,45 +1,75 @@ -$row-hover: #f4f8fe; -$gl-text-color: #54565b; -$gl-text-green: #4a2; -$gl-text-red: #d12f19; -$gl-text-orange: #d90; -$gl-header-color: #323232; -$gl-link-color: #333c48; -$md-text-color: #444; -$md-link-color: #3084bb; -$progress-color: #c0392b; -$gl-font-size: 15px; -$list-font-size: 15px; +/* + * Layout + */ $sidebar_collapsed_width: 62px; $sidebar_width: 230px; $gutter_collapsed_width: 62px; $gutter_width: 290px; $gutter_inner_width: 258px; -$avatar_radius: 50%; + +/* + * UI elements + */ +$border-color: #efeff1; +$table-border-color: #eef0f2; +$background-color: #faf9f9; + +/* + * Text + */ +$gl-font-size: 15px; +$gl-title-color: #333; +$gl-text-color: #555; +$gl-text-green: #4a2; +$gl-text-red: #d12f19; +$gl-text-orange: #d90; +$gl-link-color: #3084bb; +$gl-dark-link-color: #333; +$gl-placeholder-color: #8f8f8f; +$gl-gray: $gl-text-color; +$gl-header-color: $gl-title-color; + +/* + * Lists + */ +$list-font-size: $gl-font-size; +$list-title-color: $gl-title-color; +$list-text-color: $gl-text-color; + +/* + * Markdown + */ +$md-text-color: $gl-text-color; +$md-link-color: $gl-link-color; + +/* + * Code + */ $code_font_size: 13px; $code_line_height: 1.5; -$border-color: #efeff1; -$table-border-color: #eef0f2; -$background-color: #faf9f9; -$header-height: 58px; -$fixed-layout-width: 1280px; -$gl-gray: #5a5a5a; + +/* + * Padding + */ $gl-padding: 16px; $gl-btn-padding: 10px; $gl-vert-padding: 6px; $gl-padding-top: 10px; + +/* + * Misc + */ +$row-hover: #f4f8fe; +$progress-color: #c0392b; +$avatar_radius: 50%; +$header-height: 58px; +$fixed-layout-width: 1280px; $gl-avatar-size: 40px; -$secondary-text: #7f8fa4; $error-exclamation-point: #e62958; $border-radius-default: 3px; -$list-title-color: #333; -$list-text-color: #555; - $btn-transparent-color: #8f8f8f; - $ssh-key-icon-color: #8f8f8f; $ssh-key-icon-size: 18px; - $provider-btn-group-border: #e5e5e5; $provider-btn-not-active-color: #4688f1; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index d57be1b2daa..33b3c7558ed 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -55,7 +55,7 @@ li.commit { } .commit-row-message { - color: $gl-link-color; + color: $gl-dark-link-color; &:hover { text-decoration: underline; diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index b39a9abf40f..84eefd01cfe 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -6,7 +6,7 @@ font-size: $gl-font-size; padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top); border-bottom: 1px solid $table-border-color; - color: #7f8fa4; + color: $list-text-color; &.event-inline { .avatar { @@ -21,7 +21,7 @@ } a { - color: #4c4e54; + color: $gl-dark-link-color; } .avatar { @@ -31,10 +31,7 @@ .event-title { @include str-truncated(calc(100% - 174px)); font-weight: 600; - - .author_name { - color: #333; - } + color: $list-text-color; } .event-body { diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 7ac4bc468d6..6a1d28590c2 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -3,7 +3,7 @@ padding: 10px $gl-padding; position: relative; - .issue-title { + .title { margin-bottom: 2px; } @@ -130,14 +130,14 @@ form.edit-issue { } .issue-closed-by-widget { - color: $secondary-text; + color: $gl-text-color; margin-left: 52px; } .editor-details { display: block; - + @media (min-width: $screen-sm-min) { display: inline-block; } -}
\ No newline at end of file +} diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 27970eba159..f983e9829e6 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -14,25 +14,8 @@ } .todo-item { - font-size: $gl-font-size; - padding-left: $gl-avatar-size + $gl-padding-top; - color: $secondary-text; - - a { - color: #4c4e54; - } - - .avatar { - margin-left: -($gl-avatar-size + $gl-padding-top); - } - .todo-title { @include str-truncated(calc(100% - 174px)); - font-weight: 600; - - .author-name { - color: #333; - } } .todo-body { diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 73c7c9f687c..25b5e95583e 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -41,7 +41,7 @@ vertical-align: middle; i, a { - color: $gl-link-color; + color: $gl-dark-link-color; } img { diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index 7857af9c5de..be488483b09 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -6,7 +6,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController end def destroy - todo.done! + todo.done todo_notice = 'Todo was successfully marked as done.' @@ -20,7 +20,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController end def destroy_all - @todos.each(&:done!) + @todos.each(&:done) respond_to do |format| format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' } diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 139e40db180..b538c7d1608 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -3,7 +3,7 @@ class DashboardController < Dashboard::ApplicationController include MergeRequestsAction before_action :event_filter, only: :activity - before_action :projects, only: [:issues, :merge_requests] + before_action :projects, only: [:issues, :merge_requests, :labels, :milestones] respond_to :html @@ -20,6 +20,29 @@ class DashboardController < Dashboard::ApplicationController end end + def labels + labels = Label.where(project_id: @projects).select(:title, :color).uniq(:title) + + respond_to do |format| + format.json do + render json: labels + end + end + end + + def milestones + milestones = Milestone.where(project_id: @projects).active + epoch = DateTime.parse('1970-01-01') + grouped_milestones = GlobalMilestone.build_collection(milestones) + grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date } + + respond_to do |format| + format.json do + render json: grouped_milestones + end + end + end + protected def load_events diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index 24025d8c723..c721dca58d9 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -7,6 +7,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController if pre_auth.authorizable? if skip_authorization? || matching_token? auth = authorization.authorize + session.delete(:user_return_to) redirect_to auth.redirect_uri else render "doorkeeper/authorizations/new" diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 40d8098690a..5f471d405f5 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -12,6 +12,13 @@ class Projects::LabelsController < Projects::ApplicationController def index @labels = @project.labels.page(params[:page]).per(PER_PAGE) + + respond_to do |format| + format.html + format.json do + render json: @project.labels + end + end end def new diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 61b82c9db46..7248ede1699 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -5,7 +5,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled before_action :merge_request, only: [ :edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check, - :ci_status, :cancel_merge_when_build_succeeds + :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip ] before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds] before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds] @@ -20,7 +20,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :authorize_create_merge_request!, only: [:new, :create] # Allow modify merge_request - before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :sort] + before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort] def index terms = params['issue_search'] @@ -164,6 +164,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + def remove_wip + MergeRequests::UpdateService.new(project, current_user, title: @merge_request.wipless_title).execute(@merge_request) + + redirect_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), + notice: "The merge request can now be merged." + end + def merge_check @merge_request.check_if_can_be_merged diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index da46731d945..0998b191c07 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -19,7 +19,15 @@ class Projects::MilestonesController < Projects::ApplicationController end @milestones = @milestones.includes(:project) - @milestones = @milestones.page(params[:page]).per(PER_PAGE) + + respond_to do |format| + format.html do + @milestones = @milestones.page(params[:page]).per(PER_PAGE) + end + format.json do + render json: @milestones + end + end end def new diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 74f326e0b83..ceff1fbb161 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -24,7 +24,7 @@ module DropdownsHelper capture(&block) if block && !options.has_key?(:footer_content) end - if block && options.has_key?(:footer_content) + if block && options[:footer_content] output << content_tag(:div, class: "dropdown-footer") do capture(&block) end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 4455dcd0e20..ed37176aa6b 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -32,7 +32,7 @@ module LabelsHelper # link_to_label(label) { "My Custom Label Text" } # # Returns a String - def link_to_label(label, project: nil, type: :issue, &block) + def link_to_label(label, project: nil, type: :issue, tooltip: true, &block) project ||= @project || label.project link = send("namespace_project_#{type.to_s.pluralize}_path", project.namespace, @@ -42,7 +42,7 @@ module LabelsHelper if block_given? link_to link, &block else - link_to render_colored_label(label), link + link_to render_colored_label(label, tooltip: tooltip), link end end @@ -50,23 +50,24 @@ module LabelsHelper @project.labels.pluck(:title) end - def render_colored_label(label, label_suffix = '') + def render_colored_label(label, label_suffix = '', tooltip: true) label_color = label.color || Label::DEFAULT_COLOR text_color = text_color_for_bg(label_color) # Intentionally not using content_tag here so that this method can be called # by LabelReferenceFilter - span = %(<span class="label color-label") + - %(style="background-color: #{label_color}; color: #{text_color}">) + + span = %(<span class="label color-label #{"has_tooltip" if tooltip}" ) + + %(style="background-color: #{label_color}; color: #{text_color}" ) + + %(title="#{escape_once(label.description)}" data-container="body">) + %(#{escape_once(label.name)}#{label_suffix}</span>) span.html_safe end - def render_colored_cross_project_label(label) + def render_colored_cross_project_label(label, tooltip: true) label_suffix = label.project.name_with_namespace label_suffix = " <i>in #{escape_once(label_suffix)}</i>" - render_colored_label(label, label_suffix) + render_colored_label(label, label_suffix, tooltip: tooltip) end def suggested_colors @@ -109,19 +110,12 @@ module LabelsHelper end end - def projects_labels_options - labels = - if @project - @project.labels - else - Label.where(project_id: @projects) - end - - grouped_labels = GlobalLabel.build_collection(labels) - grouped_labels.unshift(Label::None) - grouped_labels.unshift(Label::Any) - - options_from_collection_for_select(grouped_labels, 'name', 'title', params[:label_name]) + def labels_filter_path + if @project + namespace_project_labels_path(@project.namespace, @project, :json) + else + labels_dashboard_path(:json) + end end def label_subscription_status(label) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 92ed0891e92..c9d8787bd19 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -46,22 +46,12 @@ module MilestonesHelper end end - def projects_milestones_options - milestones = - if @project - @project.milestones - else - Milestone.where(project_id: @projects) - end.active - - epoch = DateTime.parse('1970-01-01') - grouped_milestones = GlobalMilestone.build_collection(milestones) - grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date } - grouped_milestones.unshift(Milestone::None) - grouped_milestones.unshift(Milestone::Any) - grouped_milestones.unshift(Milestone::Upcoming) - - options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title]) + def milestones_filter_dropdown_path + if @project + namespace_project_milestones_path(@project.namespace, @project, :json) + else + milestones_dashboard_path(:json) + end end def milestone_remaining_days(milestone) diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 07ddc691d85..edc5686cf08 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -16,14 +16,19 @@ module TodosHelper def todo_target_link(todo) target = todo.target_type.titleize.downcase - link_to "#{target} #{todo.target.to_reference}", todo_target_path(todo), { title: h(todo.target.title) } + link_to "#{target} #{todo.target_reference}", todo_target_path(todo), { title: todo.target.title } end def todo_target_path(todo) anchor = dom_id(todo.note) if todo.note.present? - polymorphic_path([todo.project.namespace.becomes(Namespace), - todo.project, todo.target], anchor: anchor) + if todo.for_commit? + namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project, + todo.target, anchor: anchor) + else + polymorphic_path([todo.project.namespace.becomes(Namespace), + todo.project, todo.target], anchor: anchor) + end end def todos_filter_params diff --git a/app/models/issue.rb b/app/models/issue.rb index 053387cffd7..5347d4fa1be 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -106,9 +106,8 @@ class Issue < ActiveRecord::Base def related_branches return [] if self.project.empty_repo? - self.project.repository.branch_names.select do |branch| - branch =~ /\A#{iid}-(?!\d+-stable)/i - end + + self.project.repository.branch_names.select { |branch| branch.end_with?("-#{iid}") } end # Reset issue events cache @@ -139,7 +138,7 @@ class Issue < ActiveRecord::Base end def to_branch_name - "#{iid}-#{title.parameterize}" + "#{title.parameterize}-#{iid}" end def can_be_worked_on?(current_user) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 30a7bd47be7..a015a9ef394 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -277,8 +277,14 @@ class MergeRequest < ActiveRecord::Base self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last end + WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze + def work_in_progress? - !!(title =~ /\A\[?WIP(\]|:| )/i) + title =~ WIP_REGEX + end + + def wipless_title + self.title.sub(WIP_REGEX, "") end def mergeable? @@ -516,7 +522,7 @@ class MergeRequest < ActiveRecord::Base end def target_sha - @target_sha ||= target_project.repository.commit(target_branch).sha + @target_sha ||= target_project.repository.commit(target_branch).try(:sha) end def source_sha @@ -572,8 +578,11 @@ class MergeRequest < ActiveRecord::Base end def compute_diverged_commits_count + return 0 unless source_sha && target_sha + Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_sha, target_sha).size end + private :compute_diverged_commits_count def diverged_from_target_branch? diverged_commits_count > 0 diff --git a/app/models/todo.rb b/app/models/todo.rb index 5f91991f781..d85f7bfdf57 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -5,14 +5,15 @@ # id :integer not null, primary key # user_id :integer not null # project_id :integer not null -# target_id :integer not null +# target_id :integer # target_type :string not null # author_id :integer -# note_id :integer # action :integer not null # state :string not null # created_at :datetime # updated_at :datetime +# note_id :integer +# commit_id :string # class Todo < ActiveRecord::Base @@ -27,7 +28,9 @@ class Todo < ActiveRecord::Base delegate :name, :email, to: :author, prefix: true, allow_nil: true - validates :action, :project, :target, :user, presence: true + validates :action, :project, :target_type, :user, presence: true + validates :target_id, presence: true, unless: :for_commit? + validates :commit_id, presence: true, if: :for_commit? default_scope { reorder(id: :desc) } @@ -36,7 +39,7 @@ class Todo < ActiveRecord::Base state_machine :state, initial: :pending do event :done do - transition [:pending, :done] => :done + transition [:pending] => :done end state :pending @@ -50,4 +53,25 @@ class Todo < ActiveRecord::Base target.title end end + + def for_commit? + target_type == "Commit" + end + + # override to return commits, which are not active record + def target + if for_commit? + project.commit(commit_id) rescue nil + else + super + end + end + + def target_reference + if for_commit? + target.short_id + else + target.to_reference + end + end end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index 7b306a8a531..ac5b58db862 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -5,6 +5,19 @@ module MergeRequests SystemNoteService.change_status(merge_request, merge_request.target_project, current_user, merge_request.state, nil) end + def create_title_change_note(issuable, old_title) + removed_wip = old_title =~ MergeRequest::WIP_REGEX && !issuable.work_in_progress? + added_wip = old_title !~ MergeRequest::WIP_REGEX && issuable.work_in_progress? + + if removed_wip + SystemNoteService.remove_merge_request_wip(issuable, issuable.project, current_user) + elsif added_wip + SystemNoteService.add_merge_request_wip(issuable, issuable.project, current_user) + else + super + end + end + def hook_data(merge_request, action) hook_data = merge_request.to_hook_data(current_user) merge_request_url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id) diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index fa34753c4fd..6e9152e444e 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -51,7 +51,7 @@ module MergeRequests # be interpreted as the use wants to close that issue on this project # Pattern example: 112-fix-mep-mep # Will lead to appending `Closes #112` to the description - if match = merge_request.source_branch.match(/\A(\d+)-/) + if match = merge_request.source_branch.match(/-(\d+)\z/) iid = match[1] closes_issue = "Closes ##{iid}" diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb index bccd67d3dbf..a0973c5d260 100644 --- a/app/services/projects/housekeeping_service.rb +++ b/app/services/projects/housekeeping_service.rb @@ -24,7 +24,7 @@ module Projects def execute raise LeaseTaken if !try_obtain_lease - GitlabShellWorker.perform_async(:gc, @project.path_with_namespace) + GitlabShellOneShotWorker.perform_async(:gc, @project.path_with_namespace) ensure @project.update_column(:pushes_since_gc, 0) end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index f09b77c4a57..c644cd0b951 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -144,6 +144,18 @@ class SystemNoteService create_note(noteable: noteable, project: project, author: author, note: body) end + def self.remove_merge_request_wip(noteable, project, author) + body = 'Unmarked this merge request as a Work In Progress' + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + def self.add_merge_request_wip(noteable, project, author) + body = 'Marked this merge request as a **Work In Progress**' + + create_note(noteable: noteable, project: project, author: author, note: body) + end + # Called when the title of a Noteable is changed # # noteable - Noteable object that responds to `title` @@ -210,7 +222,7 @@ class SystemNoteService # Called when a branch is created from the 'new branch' button on a issue # Example note text: # - # "Started branch `201-issue-branch-button`" + # "Started branch `issue-branch-button-201`" def self.new_issue_branch(issue, project, author, branch) h = Gitlab::Application.routes.url_helpers link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch) diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 4392e2d17fe..f2662922e90 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -103,24 +103,16 @@ class TodoService # * mark all pending todos related to the target for the current user as done # def mark_pending_todos_as_done(target, user) - pending_todos(user, target.project, target).update_all(state: :done) + attributes = attributes_for_target(target) + pending_todos(user, attributes).update_all(state: :done) end private - def create_todos(project, target, author, users, action, note = nil) + def create_todos(users, attributes) Array(users).each do |user| - next if pending_todos(user, project, target).exists? - - Todo.create( - project: project, - user_id: user.id, - author_id: author.id, - target_id: target.id, - target_type: target.class.name, - action: action, - note: note - ) + next if pending_todos(user, attributes).exists? + Todo.create(attributes.merge(user_id: user.id)) end end @@ -130,8 +122,8 @@ class TodoService end def handle_note(note, author) - # Skip system notes, notes on commit, and notes on project snippet - return if note.system? || ['Commit', 'Snippet'].include?(note.noteable_type) + # Skip system notes, and notes on project snippet + return if note.system? || note.for_project_snippet? project = note.project target = note.noteable @@ -142,13 +134,39 @@ class TodoService def create_assignment_todo(issuable, author) if issuable.assignee && issuable.assignee != author - create_todos(issuable.project, issuable, author, issuable.assignee, Todo::ASSIGNED) + attributes = attributes_for_todo(issuable.project, issuable, author, Todo::ASSIGNED) + create_todos(issuable.assignee, attributes) end end - def create_mention_todos(project, issuable, author, note = nil) - mentioned_users = filter_mentioned_users(project, note || issuable, author) - create_todos(project, issuable, author, mentioned_users, Todo::MENTIONED, note) + def create_mention_todos(project, target, author, note = nil) + mentioned_users = filter_mentioned_users(project, note || target, author) + attributes = attributes_for_todo(project, target, author, Todo::MENTIONED, note) + create_todos(mentioned_users, attributes) + end + + def attributes_for_target(target) + attributes = { + project_id: target.project.id, + target_id: target.id, + target_type: target.class.name, + commit_id: nil + } + + if target.is_a?(Commit) + attributes.merge!(target_id: nil, commit_id: target.id) + end + + attributes + end + + def attributes_for_todo(project, target, author, action, note = nil) + attributes_for_target(target).merge!( + project_id: project.id, + author_id: author.id, + action: action, + note: note + ) end def filter_mentioned_users(project, target, author) @@ -160,11 +178,8 @@ class TodoService mentioned_users.uniq end - def pending_todos(user, project, target) - user.todos.pending.where( - project_id: project.id, - target_id: target.id, - target_type: target.class.name - ) + def pending_todos(user, criteria = {}) + valid_keys = [:project_id, :target_id, :target_type, :commit_id] + user.todos.pending.where(criteria.slice(*valid_keys)) end end diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml index 5736a301910..f417b2e44a4 100644 --- a/app/views/admin/labels/_label.html.haml +++ b/app/views/admin/labels/_label.html.haml @@ -1,6 +1,6 @@ %li{id: dom_id(label)} .label-row - = render_colored_label(label) + = render_colored_label(label, tooltip: false) = markdown(label.description, pipeline: :single_line) .pull-right = link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm' diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index 4c848a50181..e3a4d64df01 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -2,7 +2,7 @@ .todo-item.todo-block = image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:'' - .todo-title + .todo-title.title %span.author-name - if todo.author = link_to_author(todo) diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 623381375a5..f9ec3a89158 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -51,7 +51,7 @@ .panel-heading = link_to project.name_with_namespace, namespace_project_path(project.namespace, project) - %ul.well-list.todos-list + %ul.content-list.todos-list = render group[1] = paginate @todos, theme: "gitlab" - else diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 77d01a7736c..f3090b96702 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -46,6 +46,8 @@ %h1.title= title = render 'shared/outdated_browser' + - if @project && !@project.empty_repo? - :javascript - var findFileURL = "#{namespace_project_find_file_path(@project.namespace, @project, @ref || @project.repository.root_ref)}"; + - if ref = @ref || @project.repository.root_ref + :javascript + var findFileURL = "#{namespace_project_find_file_path(@project.namespace, @project, ref)}"; diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 6086ad3661e..2e1a37aa06d 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -20,4 +20,4 @@ - next unless blob = render 'projects/diffs/file', i: index, project: project, - diff_file: diff_file, diff_commit: diff_commit, blob: blob + diff_file: diff_file, diff_commit: diff_commit, blob: blob, diff_refs: diff_refs diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index dc34032b1b8..3898bb202c5 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -53,6 +53,6 @@ = render "projects/diffs/text_file", diff_file: diff_file, index: i - elsif blob.image? - old_file = project.repository.prev_blob_for_diff(diff_commit, diff_file) - = render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i + = render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i, diff_refs: diff_refs - else .nothing-here-block No preview for this file type diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml index 752e92e2e6b..8367112a9cb 100644 --- a/app/views/projects/diffs/_image.html.haml +++ b/app/views/projects/diffs/_image.html.haml @@ -1,6 +1,7 @@ - diff = diff_file.diff - file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path)) -- old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.parent_id, diff.old_path)) +- old_commit_id = diff_refs.first.id +- old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path)) - if diff.renamed_file || diff.new_file || diff.deleted_file .image %span.wrap @@ -12,7 +13,7 @@ %div.two-up.view %span.wrap .frame.deleted - %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.parent_id, diff.old_path))} + %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path))} %img{src: old_file_raw_path} %p.image-info.hide %span.meta-filesize= "#{number_to_human_size old_file.size}" diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 00e1a3d8069..4aa92d0b39e 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -3,11 +3,11 @@ .issue-check = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue" - .issue-title + .issue-title.title %span.issue-title-text = confidential_icon(issue) - = link_to_gfm issue.title, issue_path(issue), class: "title" - %ul.controls.light + = link_to_gfm issue.title, issue_path(issue) + %ul.controls - if issue.closed? %li CLOSED diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index ce5b84ee712..52df3de8a27 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -32,9 +32,9 @@ = time_ago_with_tooltip(@issue.created_at) by %strong - = link_to_member(@project, @issue.author, avatar: false, size: 24, mobile_classes: "hidden-xs") + = link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-xs") %strong - = link_to_member(@project, @issue.author, avatar: false, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg", + = link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg", by_username: true, avatar: false) .pull-right.issue-btn-group diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 18cf3f14f0b..13d0cbdde1d 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -1,8 +1,8 @@ %li{ class: mr_css_classes(merge_request) } - .merge-request-title + .merge-request-title.title %span.merge-request-title-text - = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "title" - %ul.controls.light + = link_to_gfm merge_request.title, merge_request_path(merge_request) + %ul.controls - if merge_request.merged? %li MERGED diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index c6cbe8589ef..eeb605e2dc5 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -19,9 +19,9 @@ = time_ago_with_tooltip(@merge_request.created_at) by %strong - = link_to_member(@project, @merge_request.author, avatar: false, size: 24, mobile_classes: "hidden-xs") + = link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-xs") %strong - = link_to_member(@project, @merge_request.author, avatar: false, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg", + = link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg", by_username: true, avatar: false) .issue-btn-group.pull-right diff --git a/app/views/projects/merge_requests/widget/open/_wip.html.haml b/app/views/projects/merge_requests/widget/open/_wip.html.haml index 0cf16542cc1..c296422a9cf 100644 --- a/app/views/projects/merge_requests/widget/open/_wip.html.haml +++ b/app/views/projects/merge_requests/widget/open/_wip.html.haml @@ -1,5 +1,11 @@ %h4 This merge request is currently a Work In Progress -%p - When this merge request is ready, remove the "WIP" prefix from the title to allow it to be merged. +- if can?(current_user, :update_merge_request, @merge_request) + %p + When this merge request is ready, + = link_to remove_wip_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), method: :post do + remove the + %code WIP: + prefix from the title + to allow it to be merged. diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index 8134b15d245..4b47b0291be 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -1,4 +1,4 @@ %span.label-row - = link_to_label(label) + = link_to_label(label, tooltip: false) %span.prepend-left-10 = markdown(label.description, pipeline: :single_line) diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index fb9a8db0889..f172350f5ff 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -10,7 +10,7 @@ %i.fa.fa-cogs = link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Leave this group' do - %i.fa.fa-sign-out + = icon('sign-out') .stats %span @@ -22,12 +22,13 @@ = number_with_delimiter(group.users.count) = image_tag group_icon(group), class: "avatar s40 hidden-xs" - = link_to group, class: 'group-name title' do - = group.name + .title + = link_to group, class: 'group-name' do + = group.name - - if group_member - as - %span #{group_member.human_access} + - if group_member + as + %span #{group_member.human_access} - if group.description.present? .description diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index dfdc84ba4cc..ac20f7d1f7e 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -10,74 +10,19 @@ - if params[:author_id] = hidden_field_tag(:author_id, params[:author_id]) = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author", - placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id" } }) + placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } }) .filter-item.inline - if params[:assignee_id] = hidden_field_tag(:assignee_id, params[:assignee_id]) = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee", - placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id" } }) + placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } }) .filter-item.inline.milestone-filter - - if params[:milestone_title] - = hidden_field_tag(:milestone_title, params[:milestone_title]) - = dropdown_tag(h(params[:milestone_name] || "Milestone"), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable", - placeholder: "Search milestones", footer_content: true, data: { show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: (@project.id if @project), milestones: (namespace_project_milestones_path(@project.namespace, @project, :js) if @project) } }) do - - if @project - %ul.dropdown-footer-list - - if can? current_user, :admin_milestone, @project - %li - = link_to new_namespace_project_milestone_path(@project.namespace, @project), title: "New Milestone" do - Create new - %li - = link_to namespace_project_milestones_path(@project.namespace, @project) do - - if can? current_user, :admin_milestone, @project - Manage milestones - - else - View milestones + = render "shared/issuable/milestone_dropdown" .filter-item.inline.labels-filter - - if params[:label_name] - = hidden_field_tag(:label_name, params[:label_name]) - .dropdown - %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: (@project.id if @project), labels: (namespace_project_labels_path(@project.namespace, @project, :js) if @project)}} - %span.dropdown-toggle-text - = h(params[:label_name] || "Label") - = icon('chevron-down') - .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable - .dropdown-page-one - = dropdown_title("Filter by label") - = dropdown_filter("Search labels") - = dropdown_content - - if @project - = dropdown_footer do - %ul.dropdown-footer-list - - if can? current_user, :admin_label, @project - %li - %a.dropdown-toggle-page{href: "#"} - Create new - %li - = link_to namespace_project_labels_path(@project.namespace, @project) do - - if can? current_user, :admin_label, @project - Manage labels - - else - View labels - - if can? current_user, :admin_label, @project - .dropdown-page-two - = dropdown_title("Create new label", back: true) - = dropdown_content do - %input#new_label_color{type: "hidden"} - %input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"} - .dropdown-label-color-preview.js-dropdown-label-color-preview - .suggest-colors.suggest-colors-dropdown - - suggested_colors.each do |color| - = link_to '#', style: "background-color: #{color}", data: { color: color } do -   - %button.btn.btn-primary.js-new-label-btn{type: "button"} - Create - = dropdown_loading - .dropdown-loading - = icon('spinner spin') + = render "shared/issuable/label_dropdown" .pull-right = render 'shared/sort_dropdown' diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 9ef729e960c..80418e69d9c 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -13,12 +13,21 @@ - if issuable.is_a?(MergeRequest) %p.help-block - - if issuable.work_in_progress? - Remove the <code>WIP</code> prefix from the title to allow this - <strong>Work In Progress</strong> merge request to be merged when it's ready. - - else - Start the title with <code>[WIP]</code> or <code>WIP:</code> to prevent a - <strong>Work In Progress</strong> merge request from being merged before it's ready. + .js-wip-explanation + %a.js-toggle-wip{href: ""} + Remove the + %code WIP: + prefix from the title + to allow this + %strong Work In Progress + merge request to be merged when it's ready. + .js-no-wip-explanation + %a.js-toggle-wip{href: ""} + Start the title with + %code WIP: + to prevent a + %strong Work In Progress + merge request from being merged before it's ready. .form-group.detail-page-description = f.label :description, 'Description', class: 'control-label' .col-sm-10 diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml new file mode 100644 index 00000000000..87617315181 --- /dev/null +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -0,0 +1,39 @@ +- if params[:label_name] + = hidden_field_tag(:label_name, params[:label_name]) +.dropdown + %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} + %span.dropdown-toggle-text + = h(params[:label_name].presence || "Label") + = icon('chevron-down') + .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable + .dropdown-page-one + = dropdown_title("Filter by label") + = dropdown_filter("Search labels") + = dropdown_content + - if @project + = dropdown_footer do + %ul.dropdown-footer-list + - if can? current_user, :admin_label, @project + %li + %a.dropdown-toggle-page{href: "#"} + Create new + %li + = link_to namespace_project_labels_path(@project.namespace, @project) do + - if can? current_user, :admin_label, @project + Manage labels + - else + View labels + - if can? current_user, :admin_label, @project and @project + .dropdown-page-two + = dropdown_title("Create new label", back: true) + = dropdown_content do + %input#new_label_color{type: "hidden"} + %input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"} + .dropdown-label-color-preview.js-dropdown-label-color-preview + .suggest-colors.suggest-colors-dropdown + - suggested_colors.each do |color| + = link_to '#', style: "background-color: #{color}", data: { color: color } do +   + %button.btn.btn-primary.js-new-label-btn{type: "button"} + Create + = dropdown_loading diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml new file mode 100644 index 00000000000..0434506c8d7 --- /dev/null +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -0,0 +1,16 @@ +- if params[:milestone_title] + = hidden_field_tag(:milestone_title, params[:milestone_title]) += dropdown_tag(h(params[:milestone_title].presence || "Milestone"), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable", + placeholder: "Search milestones", footer_content: @project.present?, data: { show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do + - if @project + %ul.dropdown-footer-list + - if can? current_user, :admin_milestone, @project + %li + = link_to new_namespace_project_milestone_path(@project.namespace, @project), title: "New Milestone" do + Create new + %li + = link_to namespace_project_milestones_path(@project.namespace, @project) do + - if can? current_user, :admin_milestone, @project + Manage milestones + - else + View milestones diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml index ba27bafd1bc..868b2357003 100644 --- a/app/views/shared/milestones/_labels_tab.html.haml +++ b/app/views/shared/milestones/_labels_tab.html.haml @@ -5,7 +5,7 @@ %li %span.label-row = link_to milestones_label_path(options) do - - render_colored_label(label) + - render_colored_label(label, tooltip: false) %span.prepend-left-10 = markdown(label.description, pipeline: :single_line) diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 97cfb76cdb0..872d2bdf46d 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -7,26 +7,11 @@ - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description - ci_commit = project.ci_commit(project.commit.sha) if ci && !project.empty_repo? && project.commit -- cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.2'] +- cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.3'] - cache_key.push(ci_commit.status) if ci_commit %li.project-row{ class: css_class } = cache(cache_key) do - = link_to project_path(project), class: dom_class(project) do - - if avatar - .dash-project-avatar - - if use_creator_avatar - = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:'' - - else - = project_icon(project, alt: '', class: 'avatar project-avatar s40') - %span.project-full-name.title - %span.namespace-name - - if project.namespace && !skip_namespace - = project.namespace.human_name - \/ - %span.project-name.filter-title - = project.name - .controls - if project.main_language %span @@ -45,6 +30,23 @@ %span.visibility-icon.has_tooltip{data: { container: 'body', placement: 'left' }, title: "#{visibility_level_label(project.visibility_level)} - #{project_visibility_level_description(project.visibility_level)}"} = visibility_level_icon(project.visibility_level, fw: false) + + .title + = link_to project_path(project), class: dom_class(project) do + - if avatar + .dash-project-avatar + - if use_creator_avatar + = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:'' + - else + = project_icon(project, alt: '', class: 'avatar project-avatar s40') + %span.project-full-name + %span.namespace-name + - if project.namespace && !skip_namespace + = project.namespace.human_name + \/ + %span.project-name.filter-title + = project.name + - if show_last_commit_as_description .description = link_to_gfm project.commit.title, namespace_project_commit_path(project.namespace, project, project.commit), diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index a316a085107..c96dfefe17f 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -1,8 +1,8 @@ %li.snippet-row = image_tag avatar_icon(snippet.author_email), class: "avatar s40 hidden-xs", alt: '' - .snippet-title - = link_to reliable_snippet_path(snippet), class: 'title' do + .title + = link_to reliable_snippet_path(snippet) do = truncate(snippet.title, length: 60) - if snippet.private? %span.label.label-gray diff --git a/app/workers/gitlab_shell_one_shot_worker.rb b/app/workers/gitlab_shell_one_shot_worker.rb new file mode 100644 index 00000000000..4ddbcf574d5 --- /dev/null +++ b/app/workers/gitlab_shell_one_shot_worker.rb @@ -0,0 +1,10 @@ +class GitlabShellOneShotWorker + include Sidekiq::Worker + include Gitlab::ShellAdapter + + sidekiq_options queue: :gitlab_shell, retry: false + + def perform(action, *arg) + gitlab_shell.send(action, *arg) + end +end diff --git a/config/routes.rb b/config/routes.rb index 2ae282f48a6..561987322b2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -351,6 +351,8 @@ Rails.application.routes.draw do get :issues get :merge_requests get :activity + get :labels + get :milestones scope module: :dashboard do resources :milestones, only: [:index, :show] @@ -621,6 +623,7 @@ Rails.application.routes.draw do post :cancel_merge_when_build_succeeds get :ci_status post :toggle_subscription + post :remove_wip end collection do diff --git a/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb b/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb new file mode 100644 index 00000000000..6871b3920df --- /dev/null +++ b/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb @@ -0,0 +1,5 @@ +class ChangeTargetIdToNullOnTodos < ActiveRecord::Migration + def change + change_column_null :todos, :target_id, true + end +end diff --git a/db/migrate/20160316204731_add_commit_id_to_todos.rb b/db/migrate/20160316204731_add_commit_id_to_todos.rb new file mode 100644 index 00000000000..ae19fdd1abd --- /dev/null +++ b/db/migrate/20160316204731_add_commit_id_to_todos.rb @@ -0,0 +1,6 @@ +class AddCommitIdToTodos < ActiveRecord::Migration + def change + add_column :todos, :commit_id, :string + add_index :todos, :commit_id + end +end diff --git a/db/schema.rb b/db/schema.rb index 7e6863ef47e..5b2f5aa3ddd 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -867,7 +867,7 @@ ActiveRecord::Schema.define(version: 20160316204731) do create_table "todos", force: :cascade do |t| t.integer "user_id", null: false t.integer "project_id", null: false - t.integer "target_id", null: false + t.integer "target_id" t.string "target_type", null: false t.integer "author_id" t.integer "action", null: false @@ -875,9 +875,11 @@ ActiveRecord::Schema.define(version: 20160316204731) do t.datetime "created_at" t.datetime "updated_at" t.integer "note_id" + t.string "commit_id" end add_index "todos", ["author_id"], name: "index_todos_on_author_id", using: :btree + add_index "todos", ["commit_id"], name: "index_todos_on_commit_id", using: :btree add_index "todos", ["note_id"], name: "index_todos_on_note_id", using: :btree add_index "todos", ["project_id"], name: "index_todos_on_project_id", using: :btree add_index "todos", ["state"], name: "index_todos_on_state", using: :btree diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index a9b79bbdb1b..762b35859b9 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -279,6 +279,8 @@ job_name: | Keyword | Required | Description | |---------------|----------|-------------| | script | yes | Defines a shell script which is executed by runner | +| image | no | Use docker image, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) | +| services | no | Use docker services, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) | | stage | no | Defines a build stage (default: `test`) | | type | no | Alias for `stage` | | only | no | Defines a list of git refs for which build is created | diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb index bd85b1d798a..7ae06c27840 100644 --- a/spec/factories/todos.rb +++ b/spec/factories/todos.rb @@ -5,14 +5,15 @@ # id :integer not null, primary key # user_id :integer not null # project_id :integer not null -# target_id :integer not null +# target_id :integer # target_type :string not null # author_id :integer -# note_id :integer # action :integer not null # state :string not null # created_at :datetime # updated_at :datetime +# note_id :integer +# commit_id :string # FactoryGirl.define do @@ -30,5 +31,10 @@ FactoryGirl.define do trait :mentioned do action { Todo::MENTIONED } end + + trait :on_commit do + commit_id RepoHelpers.sample_commit.id + target_type "Commit" + end end end diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index 4f129eca183..eca8bc8ab2d 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -11,7 +11,7 @@ describe LabelsHelper do end it 'uses the instance variable' do - expect(link_to_label(label)).to match %r{<a href="/#{@project.to_reference}/issues\?label_name=#{label.name}">.*</a>} + expect(link_to_label(label)).to match %r{<a href="/#{@project.to_reference}/issues\?label_name=#{label.name}"><span class="[\w\s\-]*has_tooltip".*</span></a>} end end @@ -39,6 +39,14 @@ describe LabelsHelper do end end + context 'with a tooltip argument' do + context 'set to false' do + it 'does not include the has_tooltip class' do + expect(link_to_label(label, tooltip: false)).not_to match %r{has_tooltip} + end + end + end + context 'with block' do it 'passes the block to link_to' do link = link_to_label(label) { 'Foo' } @@ -49,7 +57,7 @@ describe LabelsHelper do context 'without block' do it 'uses render_colored_label as the link content' do expect(self).to receive(:render_colored_label). - with(label).and_return('Foo') + with(label, tooltip: true).and_return('Foo') expect(link_to_label(label)).to match('Foo') end end diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index e2d21f53b7e..4c1d4a2d24c 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -56,7 +56,7 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do describe 'label span element' do it 'includes default classes' do doc = reference_filter("Label #{reference}") - expect(doc.css('a span').first.attr('class')).to eq 'label color-label' + expect(doc.css('a span').first.attr('class')).to eq 'label color-label has_tooltip' end it 'includes a style attribute' do diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 2ccdec1eeff..540a62eb1f8 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -131,7 +131,7 @@ describe Issue, models: true do end describe '#related_branches' do - it "should " do + it "selects the right branches" do allow(subject.project.repository).to receive(:branch_names). and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name]) @@ -151,10 +151,10 @@ describe Issue, models: true do end describe "#to_branch_name" do - let(:issue) { build(:issue, title: 'a' * 30) } + let(:issue) { create(:issue, title: 'a' * 30) } it "starts with the issue iid" do - expect(issue.to_branch_name).to match /\A#{issue.iid}-a+\z/ + expect(issue.to_branch_name).to match /-#{issue.iid}\z/ end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 654c71b6825..f2f07e4ee17 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -86,6 +86,16 @@ describe MergeRequest, models: true do end end + describe '#target_sha' do + context 'when the target branch does not exist anymore' do + subject { create(:merge_request).tap { |mr| mr.update_attribute(:target_branch, 'deleted') } } + + it 'returns nil' do + expect(subject.target_sha).to be_nil + end + end + end + describe '#source_sha' do let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) } @@ -206,24 +216,11 @@ describe MergeRequest, models: true do end describe "#work_in_progress?" do - it "detects the 'WIP ' prefix" do - subject.title = "WIP #{subject.title}" - expect(subject).to be_work_in_progress - end - - it "detects the 'WIP: ' prefix" do - subject.title = "WIP: #{subject.title}" - expect(subject).to be_work_in_progress - end - - it "detects the '[WIP] ' prefix" do - subject.title = "[WIP] #{subject.title}" - expect(subject).to be_work_in_progress - end - - it "detects the '[WIP]' prefix" do - subject.title = "[WIP]#{subject.title}" - expect(subject).to be_work_in_progress + ['WIP ', 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', ' [WIP] WIP [WIP] WIP: WIP '].each do |wip_prefix| + it "detects the '#{wip_prefix}' prefix" do + subject.title = "#{wip_prefix}#{subject.title}" + expect(subject).to be_work_in_progress + end end it "doesn't detect WIP for words starting with WIP" do @@ -231,6 +228,11 @@ describe MergeRequest, models: true do expect(subject).not_to be_work_in_progress end + it "doesn't detect WIP for words containing with WIP" do + subject.title = "WupWipwap #{subject.title}" + expect(subject).not_to be_work_in_progress + end + it "doesn't detect WIP by default" do expect(subject).not_to be_work_in_progress end @@ -310,6 +312,18 @@ describe MergeRequest, models: true do let(:project) { create(:project) } let(:fork_project) { create(:project, forked_from_project: project) } + context 'when the target branch does not exist anymore' do + subject { create(:merge_request).tap { |mr| mr.update_attribute(:target_branch, 'deleted') } } + + it 'does not crash' do + expect{ subject.diverged_commits_count }.not_to raise_error + end + + it 'returns 0' do + expect(subject.diverged_commits_count).to eq(0) + end + end + context 'diverged on same repository' do subject(:merge_request_with_divergence) { create(:merge_request, :diverged, source_project: project, target_project: project) } diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb index fe9ea7e7d1e..d9b86b9368f 100644 --- a/spec/models/todo_spec.rb +++ b/spec/models/todo_spec.rb @@ -5,19 +5,24 @@ # id :integer not null, primary key # user_id :integer not null # project_id :integer not null -# target_id :integer not null +# target_id :integer # target_type :string not null # author_id :integer -# note_id :integer # action :integer not null # state :string not null # created_at :datetime # updated_at :datetime +# note_id :integer +# commit_id :string # require 'spec_helper' describe Todo, models: true do + let(:project) { create(:project) } + let(:commit) { project.commit } + let(:issue) { create(:issue) } + describe 'relationships' do it { is_expected.to belong_to(:author).class_name("User") } it { is_expected.to belong_to(:note) } @@ -33,8 +38,22 @@ describe Todo, models: true do describe 'validations' do it { is_expected.to validate_presence_of(:action) } - it { is_expected.to validate_presence_of(:target) } + it { is_expected.to validate_presence_of(:target_type) } it { is_expected.to validate_presence_of(:user) } + + context 'for commits' do + subject { described_class.new(target_type: 'Commit') } + + it { is_expected.to validate_presence_of(:commit_id) } + it { is_expected.not_to validate_presence_of(:target_id) } + end + + context 'for issuables' do + subject { described_class.new(target: issue) } + + it { is_expected.to validate_presence_of(:target_id) } + it { is_expected.not_to validate_presence_of(:commit_id) } + end end describe '#body' do @@ -55,15 +74,69 @@ describe Todo, models: true do end end - describe '#done!' do + describe '#done' do it 'changes state to done' do todo = create(:todo, state: :pending) - expect { todo.done! }.to change(todo, :state).from('pending').to('done') + expect { todo.done }.to change(todo, :state).from('pending').to('done') end it 'does not raise error when is already done' do todo = create(:todo, state: :done) - expect { todo.done! }.not_to raise_error + expect { todo.done }.not_to raise_error + end + end + + describe '#for_commit?' do + it 'returns true when target is a commit' do + subject.target_type = 'Commit' + expect(subject.for_commit?).to eq true + end + + it 'returns false when target is an issuable' do + subject.target_type = 'Issue' + expect(subject.for_commit?).to eq false + end + end + + describe '#target' do + context 'for commits' do + it 'returns an instance of Commit when exists' do + subject.project = project + subject.target_type = 'Commit' + subject.commit_id = commit.id + + expect(subject.target).to be_a(Commit) + expect(subject.target).to eq commit + end + + it 'returns nil when does not exists' do + subject.project = project + subject.target_type = 'Commit' + subject.commit_id = 'xxxx' + + expect(subject.target).to be_nil + end + end + + it 'returns the issuable for issuables' do + subject.target_id = issue.id + subject.target_type = issue.class.name + expect(subject.target).to eq issue + end + end + + describe '#target_reference' do + it 'returns the short commit id for commits' do + subject.project = project + subject.target_type = 'Commit' + subject.commit_id = commit.id + + expect(subject.target_reference).to eq commit.short_id + end + + it 'returns reference for issuables' do + subject.target = issue + expect(subject.target_reference).to eq issue.to_reference end end end diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb index 93bf1b81fbe..4c5ced7e746 100644 --- a/spec/services/projects/housekeeping_service_spec.rb +++ b/spec/services/projects/housekeeping_service_spec.rb @@ -12,7 +12,7 @@ describe Projects::HousekeepingService do it 'enqueues a sidekiq job' do expect(subject).to receive(:try_obtain_lease).and_return(true) - expect(GitlabShellWorker).to receive(:perform_async).with(:gc, project.path_with_namespace) + expect(GitlabShellOneShotWorker).to receive(:perform_async).with(:gc, project.path_with_namespace) subject.execute expect(project.pushes_since_gc).to eq(0) @@ -20,7 +20,7 @@ describe Projects::HousekeepingService do it 'does not enqueue a job when no lease can be obtained' do expect(subject).to receive(:try_obtain_lease).and_return(false) - expect(GitlabShellWorker).not_to receive(:perform_async) + expect(GitlabShellOneShotWorker).not_to receive(:perform_async) expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken) expect(project.pushes_since_gc).to eq(0) diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 96420acb31d..b4728807b8b 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -148,8 +148,13 @@ describe TodoService, services: true do should_not_create_todo(user: stranger, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) end - it 'does not create todo when leaving a note on commit' do - should_not_create_any_todo { service.new_note(note_on_commit, john_doe) } + it 'creates a todo for each valid mentioned user when leaving a note on commit' do + service.new_note(note_on_commit, john_doe) + + should_create_todo(user: michael, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) + should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) + should_not_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) + should_not_create_todo(user: stranger, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) end it 'does not create todo when leaving a note on snippet' do |