summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG15
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock6
-rw-r--r--README.md2
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee3
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee12
-rw-r--r--app/assets/javascripts/issuable_form.js.coffee41
-rw-r--r--app/assets/javascripts/issue.js.coffee25
-rw-r--r--app/assets/javascripts/issues.js.coffee26
-rw-r--r--app/assets/javascripts/labels_select.js.coffee76
-rw-r--r--app/assets/javascripts/milestone_select.js.coffee51
-rw-r--r--app/assets/javascripts/notes.js.coffee4
-rw-r--r--app/assets/javascripts/todos.js.coffee56
-rw-r--r--app/assets/javascripts/users_select.js.coffee35
-rw-r--r--app/assets/stylesheets/framework/buttons.scss10
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss6
-rw-r--r--app/assets/stylesheets/framework/filters.scss17
-rw-r--r--app/assets/stylesheets/framework/lists.scss11
-rw-r--r--app/assets/stylesheets/framework/variables.scss80
-rw-r--r--app/assets/stylesheets/pages/commits.scss2
-rw-r--r--app/assets/stylesheets/pages/diff.scss10
-rw-r--r--app/assets/stylesheets/pages/events.scss9
-rw-r--r--app/assets/stylesheets/pages/issuable.scss141
-rw-r--r--app/assets/stylesheets/pages/issues.scss8
-rw-r--r--app/assets/stylesheets/pages/projects.scss7
-rw-r--r--app/assets/stylesheets/pages/todos.scss17
-rw-r--r--app/assets/stylesheets/pages/tree.scss2
-rw-r--r--app/controllers/dashboard/todos_controller.rb17
-rw-r--r--app/controllers/dashboard_controller.rb25
-rw-r--r--app/controllers/oauth/authorizations_controller.rb1
-rw-r--r--app/controllers/projects/issues_controller.rb8
-rw-r--r--app/controllers/projects/labels_controller.rb7
-rw-r--r--app/controllers/projects/merge_requests_controller.rb11
-rw-r--r--app/controllers/projects/milestones_controller.rb10
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/finders/issues_finder.rb6
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/dropdowns_helper.rb2
-rw-r--r--app/helpers/events_helper.rb2
-rw-r--r--app/helpers/issues_helper.rb4
-rw-r--r--app/helpers/labels_helper.rb34
-rw-r--r--app/helpers/milestones_helper.rb24
-rw-r--r--app/helpers/projects_helper.rb4
-rw-r--r--app/helpers/todos_helper.rb11
-rw-r--r--app/models/ability.rb17
-rw-r--r--app/models/concerns/milestoneish.rb20
-rw-r--r--app/models/event.rb6
-rw-r--r--app/models/issue.rb14
-rw-r--r--app/models/merge_request.rb19
-rw-r--r--app/models/milestone.rb4
-rw-r--r--app/models/repository.rb18
-rw-r--r--app/models/todo.rb32
-rw-r--r--app/services/commits/revert_service.rb7
-rw-r--r--app/services/merge_requests/base_service.rb13
-rw-r--r--app/services/merge_requests/build_service.rb2
-rw-r--r--app/services/projects/autocomplete_service.rb6
-rw-r--r--app/services/projects/housekeeping_service.rb2
-rw-r--r--app/services/search/global_service.rb2
-rw-r--r--app/services/search/project_service.rb3
-rw-r--r--app/services/system_note_service.rb14
-rw-r--r--app/services/todo_service.rb65
-rw-r--r--app/views/admin/labels/_label.html.haml2
-rw-r--r--app/views/dashboard/todos/_todo.html.haml6
-rw-r--r--app/views/dashboard/todos/index.html.haml14
-rw-r--r--app/views/events/_event.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml6
-rw-r--r--app/views/layouts/nav/_project.html.haml2
-rw-r--r--app/views/projects/diffs/_diffs.html.haml2
-rw-r--r--app/views/projects/diffs/_file.html.haml22
-rw-r--r--app/views/projects/diffs/_image.html.haml5
-rw-r--r--app/views/projects/issues/_issue.html.haml7
-rw-r--r--app/views/projects/issues/show.html.haml8
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml6
-rw-r--r--app/views/projects/merge_requests/show/_mr_title.html.haml13
-rw-r--r--app/views/projects/merge_requests/widget/open/_wip.html.haml10
-rw-r--r--app/views/projects/milestones/show.html.haml2
-rw-r--r--app/views/projects/repositories/_download_archive.html.haml2
-rw-r--r--app/views/search/results/_issue.html.haml1
-rw-r--r--app/views/shared/_label_row.html.haml2
-rw-r--r--app/views/shared/groups/_group.html.haml13
-rw-r--r--app/views/shared/issuable/_filter.html.haml63
-rw-r--r--app/views/shared/issuable/_form.html.haml30
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml39
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml16
-rw-r--r--app/views/shared/issuable/_participants.html.haml16
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml48
-rw-r--r--app/views/shared/milestones/_issuable.html.haml2
-rw-r--r--app/views/shared/milestones/_labels_tab.html.haml2
-rw-r--r--app/views/shared/milestones/_milestone.html.haml4
-rw-r--r--app/views/shared/milestones/_summary.html.haml8
-rw-r--r--app/views/shared/milestones/_tabs.html.haml4
-rw-r--r--app/views/shared/milestones/_top.html.haml4
-rw-r--r--app/views/shared/projects/_project.html.haml34
-rw-r--r--app/views/shared/snippets/_snippet.html.haml4
-rw-r--r--app/workers/gitlab_shell_one_shot_worker.rb10
-rw-r--r--config/routes.rb3
-rw-r--r--db/migrate/20160223192159_add_confidential_to_issues.rb6
-rw-r--r--db/migrate/20160316192622_change_target_id_to_null_on_todos.rb5
-rw-r--r--db/migrate/20160316204731_add_commit_id_to_todos.rb6
-rw-r--r--db/schema.rb8
-rw-r--r--doc/README.md37
-rw-r--r--doc/ci/yaml/README.md2
-rw-r--r--doc/hooks/custom_hooks.md2
-rw-r--r--doc/install/installation.md10
-rw-r--r--doc/release/security.md2
-rw-r--r--doc/security/two_factor_authentication.md2
-rw-r--r--doc/update/8.2-to-8.3.md9
-rw-r--r--doc/update/8.3-to-8.4.md9
-rw-r--r--doc/update/8.4-to-8.5.md9
-rw-r--r--doc/update/8.5-to-8.6.md9
-rw-r--r--doc/workflow/importing/import_projects_from_bitbucket.md2
-rw-r--r--doc/workflow/protected_branches.md2
-rw-r--r--features/project/issues/award_emoji.feature11
-rw-r--r--features/project/merge_requests.feature8
-rw-r--r--features/steps/dashboard/todos.rb1
-rw-r--r--features/steps/groups.rb2
-rw-r--r--features/steps/project/issues/award_emoji.rb4
-rw-r--r--lib/api/issues.rb3
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb5
-rw-r--r--lib/gitlab/diff/file.rb4
-rw-r--r--lib/gitlab/project_search_results.rb3
-rw-r--r--lib/gitlab/search_results.rb7
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb162
-rw-r--r--spec/factories/issues.rb4
-rw-r--r--spec/factories/merge_requests.rb5
-rw-r--r--spec/factories/todos.rb10
-rw-r--r--spec/features/issues/new_branch_button_spec.rb2
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb2
-rw-r--r--spec/helpers/labels_helper_spec.rb12
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/redactor_filter_spec.rb72
-rw-r--r--spec/lib/gitlab/closing_issue_extractor_spec.rb1
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb14
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb69
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb2
-rw-r--r--spec/lib/gitlab/search_results_spec.rb91
-rw-r--r--spec/models/commit_spec.rb13
-rw-r--r--spec/models/concerns/mentionable_spec.rb5
-rw-r--r--spec/models/concerns/milestoneish_spec.rb104
-rw-r--r--spec/models/event_spec.rb36
-rw-r--r--spec/models/issue_spec.rb6
-rw-r--r--spec/models/merge_request_spec.rb76
-rw-r--r--spec/models/milestone_spec.rb20
-rw-r--r--spec/models/repository_spec.rb30
-rw-r--r--spec/models/todo_spec.rb85
-rw-r--r--spec/requests/api/issues_spec.rb112
-rw-r--r--spec/services/git_push_service_spec.rb4
-rw-r--r--spec/services/projects/autocomplete_service_spec.rb79
-rw-r--r--spec/services/projects/housekeeping_service_spec.rb4
-rw-r--r--spec/services/todo_service_spec.rb9
-rw-r--r--spec/support/mentionable_shared_examples.rb2
151 files changed, 2047 insertions, 654 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 0c3072e9d7f..d3171da81c3 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,12 +1,16 @@
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)
- Bump Capybara gem to 2.6.2 (Stan Hu)
- 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.
@@ -14,10 +18,12 @@ v 8.6.0 (unreleased)
- Add support for wiki with UTF-8 page names (Hiroyuki Sato)
- Fix wiki search results point to raw source (Hiroyuki Sato)
- Don't load all of GitLab in mail_room
+ - HTTP error pages work independently from location and config (Artem Sidorenko)
- Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set
- 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
@@ -37,16 +43,23 @@ v 8.6.0 (unreleased)
- Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio)
- Don't show Issues/MRs from archived projects in Groups view
- Fix wrong "iid of max iid" in Issuable sidebar for some merged MRs
+ - Fix empty source_sha on Merge Request when there is no diff (Pierre de La Morinerie)
- 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
- Move group activity to separate page
- Create external users which are excluded of internal and private projects unless access was explicitly granted
- 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
v 8.5.7
- Bump Git version requirement to 2.7.3
@@ -59,8 +72,6 @@ v 8.5.5
- Prevent a 500 error in Todos when author was removed
- Fix pagination for filtered dashboard and explore pages
- Fix "Show all" link behavior
- - Add #upcoming filter to Milestone filter (Tiago Botelho)
- - HTTP error pages work independently from location and config (Artem Sidorenko)
v 8.5.4
- Do not cache requests for badges (including builds badge)
diff --git a/Gemfile b/Gemfile
index a3fb6779e9a..e500bfb7885 100644
--- a/Gemfile
+++ b/Gemfile
@@ -51,7 +51,7 @@ gem "browser", '~> 1.0.0'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem "gitlab_git", '~> 9.0'
+gem "gitlab_git", '~> 10.0'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
diff --git a/Gemfile.lock b/Gemfile.lock
index 7b0dd83da52..63ed9441c62 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -359,11 +359,11 @@ GEM
posix-spawn (~> 0.3)
gitlab_emoji (0.3.1)
gemojione (~> 2.2, >= 2.2.1)
- gitlab_git (9.0.3)
+ gitlab_git (10.0.0)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
- rugged (~> 0.24.0b13)
+ rugged (~> 0.24.0)
gitlab_meta (7.0)
gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9)
@@ -942,7 +942,7 @@ DEPENDENCIES
github-markup (~> 1.3.1)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab_emoji (~> 0.3.0)
- gitlab_git (~> 9.0)
+ gitlab_git (~> 10.0)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.1.0)
diff --git a/README.md b/README.md
index 208427fcf8c..afa60116ebb 100644
--- a/README.md
+++ b/README.md
@@ -68,7 +68,7 @@ GitLab is a Ruby on Rails application that runs on the following software:
- Ubuntu/Debian/CentOS/RHEL
- Ruby (MRI) 2.1
-- Git 2.7.3+
+- Git 2.7.4+
- Redis 2.8+
- MySQL or PostgreSQL
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index 1be86e3b820..f5e1ca9860d 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -14,7 +14,6 @@ class Dispatcher
path = page.split(':')
shortcut_handler = null
-
switch page
when 'projects:issues:index'
Issues.init()
@@ -25,6 +24,8 @@ class Dispatcher
new ZenMode()
when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
new Milestone()
+ when 'dashboard:todos:index'
+ new Todos()
when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode()
new DropzoneInput($('.milestone-form'))
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
index 4f038477755..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
@@ -246,11 +250,15 @@ class GitLabDropdown
if oldValue
value = "#{oldValue},#{value}"
else
- @dropdown.find(ACTIVE_CLASS).removeClass ACTIVE_CLASS
+ @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
# Toggle active class for the tick mark
el.toggleClass "is-active"
+ # Toggle the dropdown label
+ if @options.toggleLabel
+ $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selectedObject)
+
if value?
if !field.length
# Create hidden input for form
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/issue.js.coffee b/app/assets/javascripts/issue.js.coffee
index d663e34871c..f50df1f5ea3 100644
--- a/app/assets/javascripts/issue.js.coffee
+++ b/app/assets/javascripts/issue.js.coffee
@@ -7,6 +7,7 @@ class @Issue
# Prevent duplicate event bindings
@disableTaskList()
@fixAffixScroll()
+ @initParticipants()
if $('a.btn-close').length
@initTaskList()
@initIssueBtnEventListeners()
@@ -84,3 +85,27 @@ class @Issue
type: 'PATCH'
url: $('form.js-issuable-update').attr('action')
data: patchData
+
+ initParticipants: ->
+ _this = @
+ $(document).on "click", ".js-participants-more", @toggleHiddenParticipants
+
+ $(".js-participants-author").each (i) ->
+ if i >= _this.PARTICIPANTS_ROW_COUNT
+ $(@)
+ .addClass "js-participants-hidden"
+ .hide()
+
+ toggleHiddenParticipants: (e) ->
+ e.preventDefault()
+
+ currentText = $(this).text().trim()
+ lessText = $(this).data("less-text")
+ originalText = $(this).data("original-text")
+
+ if currentText is originalText
+ $(this).text(lessText)
+ else
+ $(this).text(originalText)
+
+ $(".js-participants-hidden").toggle()
diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee
index a0acf3028bf..1127b289264 100644
--- a/app/assets/javascripts/issues.js.coffee
+++ b/app/assets/javascripts/issues.js.coffee
@@ -41,24 +41,28 @@
@timer = null
$("#issue_search").keyup ->
clearTimeout(@timer)
- @timer = setTimeout(Issues.filterResults, 500)
+ @timer = setTimeout( ->
+ Issues.filterResults $("#issue_search_form")
+ , 500)
- filterResults: =>
- form = $("#issue_search_form")
- search = $("#issue_search").val()
- $('.issues-holder').css("opacity", '0.5')
- issues_url = form.attr('action') + '?' + form.serialize()
+ filterResults: (form) =>
+ $('.issues-holder, .merge-requests-holder').css("opacity", '0.5')
+ formAction = form.attr('action')
+ formData = form.serialize()
+ issuesUrl = formAction
+ issuesUrl += ("#{if formAction.indexOf("?") < 0 then '?' else '&'}")
+ issuesUrl += formData
$.ajax
type: "GET"
- url: form.attr('action')
- data: form.serialize()
+ url: formAction
+ data: formData
complete: ->
- $('.issues-holder').css("opacity", '1.0')
+ $('.issues-holder, .merge-requests-holder').css("opacity", '1.0')
success: (data) ->
- $('.issues-holder').html(data.html)
+ $('.issues-holder, .merge-requests-holder').html(data.html)
# Change url so if user reload a page - search results are saved
- history.replaceState {page: issues_url}, document.title, issues_url
+ history.replaceState {page: issuesUrl}, document.title, issuesUrl
Issues.reload()
dataType: "json"
diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee
index 5ade2cb66cb..4a0c18a99a6 100644
--- a/app/assets/javascripts/labels_select.js.coffee
+++ b/app/assets/javascripts/labels_select.js.coffee
@@ -1,30 +1,32 @@
class @LabelsSelect
constructor: ->
$('.js-label-select').each (i, dropdown) ->
- projectId = $(dropdown).data('project-id')
- labelUrl = $(dropdown).data("labels")
- selectedLabel = $(dropdown).data('selected')
+ $dropdown = $(dropdown)
+ projectId = $dropdown.data('project-id')
+ labelUrl = $dropdown.data('labels')
+ selectedLabel = $dropdown.data('selected')
if selectedLabel
- selectedLabel = selectedLabel.split(",")
+ selectedLabel = selectedLabel.split(',')
newLabelField = $('#new_label_name')
newColorField = $('#new_label_color')
- showNo = $(dropdown).data('show-no')
- showAny = $(dropdown).data('show-any')
+ showNo = $dropdown.data('show-no')
+ showAny = $dropdown.data('show-any')
+ defaultLabel = $dropdown.data('default-label')
if newLabelField.length
- $('.suggest-colors-dropdown a').on "click", (e) ->
+ $('.suggest-colors-dropdown a').on 'click', (e) ->
e.preventDefault()
e.stopPropagation()
- newColorField.val $(this).data("color")
+ newColorField.val $(this).data('color')
$('.js-dropdown-label-color-preview')
- .css 'background-color', $(this).data("color")
+ .css 'background-color', $(this).data('color')
.addClass 'is-active'
- $('.js-new-label-btn').on "click", (e) ->
+ $('.js-new-label-btn').on 'click', (e) ->
e.preventDefault()
e.stopPropagation()
- if newLabelField.val() isnt "" && newColorField.val() isnt ""
+ if newLabelField.val() isnt '' and newColorField.val() isnt ''
$('.js-new-label-btn').disable()
# Create new label with API
@@ -33,46 +35,38 @@ class @LabelsSelect
color: newColorField.val()
}, (label) ->
$('.js-new-label-btn').enable()
- $('.dropdown-menu-back', $(dropdown).parent()).trigger "click"
+ $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
- $(dropdown).glDropdown(
+ $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"
- title: 'No label'
+ id: 0
+ title: 'No Label'
)
if showAny
data.unshift(
- title: 'Any label'
+ isAny: true
+ title: 'Any Label'
)
if data.length > 2
- data.splice 2, 0, "divider"
+ data.splice 2, 0, 'divider'
callback data
renderRow: (label) ->
if $.isArray(selectedLabel)
- selected = ""
+ selected = ''
$.each selectedLabel, (i, selectedLbl) ->
selectedLbl = selectedLbl.trim()
- if selected is "" && label.title is selectedLbl
- selected = "is-active"
+ if selected is '' and label.title is selectedLbl
+ selected = 'is-active'
else
- selected = if label.title is selectedLabel then "is-active" else ""
+ selected = if label.title is selectedLabel then 'is-active' else ''
"<li>
<a href='#' class='#{selected}'>
@@ -83,10 +77,24 @@ class @LabelsSelect
search:
fields: ['title']
selectable: true
- fieldName: $(dropdown).data('field-name')
+ toggleLabel: (selected) ->
+ if selected and selected.title isnt 'Any Label'
+ selected.title
+ else
+ defaultLabel
+ fieldName: $dropdown.data('field-name')
id: (label) ->
- label.title
+ if label.isAny?
+ ''
+ else
+ label.title
clicked: ->
- if $(dropdown).hasClass "js-filter-submit"
- $(dropdown).parents('form').submit()
+ page = $('body').data 'page'
+ isIssueIndex = page is 'projects:issues:index'
+ isMRIndex = page is page is 'projects:merge_requests:index'
+
+ if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
+ Issues.filterResults $dropdown.closest('form')
+ else if $dropdown.hasClass 'js-filter-submit'
+ $dropdown.closest('form').submit()
)
diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee
index 5e884454a65..e17a1adb648 100644
--- a/app/assets/javascripts/milestone_select.js.coffee
+++ b/app/assets/javascripts/milestone_select.js.coffee
@@ -1,60 +1,65 @@
class @MilestoneSelect
constructor: ->
$('.js-milestone-select').each (i, dropdown) ->
- projectId = $(dropdown).data('project-id')
- milestonesUrl = $(dropdown).data('milestones')
- selectedMilestone = $(dropdown).data('selected')
- showNo = $(dropdown).data('show-no')
- showAny = $(dropdown).data('show-any')
- useId = $(dropdown).data('use-id')
+ $dropdown = $(dropdown)
+ projectId = $dropdown.data('project-id')
+ milestonesUrl = $dropdown.data('milestones')
+ selectedMilestone = $dropdown.data('selected')
+ showNo = $dropdown.data('show-no')
+ showAny = $dropdown.data('show-any')
+ useId = $dropdown.data('use-id')
+ defaultLabel = $dropdown.data('default-label')
- $(dropdown).glDropdown(
+ $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"
+ id: '0'
title: 'No Milestone'
)
if showAny
data.unshift(
+ isAny: true
title: 'Any Milestone'
)
if data.length > 2
- data.splice 2, 0, "divider"
+ data.splice 2, 0, 'divider'
callback(data)
filterable: true
search:
fields: ['title']
selectable: true
- fieldName: $(dropdown).data('field-name')
+ toggleLabel: (selected) ->
+ if selected && 'id' of selected
+ selected.title
+ else
+ defaultLabel
+ fieldName: $dropdown.data('field-name')
text: (milestone) ->
milestone.title
id: (milestone) ->
if !useId
- if milestone.title isnt "Any milestone"
+ if !milestone.isAny?
milestone.title
else
- ""
+ ''
else
milestone.id
isSelected: (milestone) ->
milestone.title is selectedMilestone
clicked: ->
- if $(dropdown).hasClass "js-filter-submit"
- $(dropdown).parents('form').submit()
+ page = $('body').data 'page'
+ isIssueIndex = page is 'projects:issues:index'
+ isMRIndex = page is page is 'projects:merge_requests:index'
+
+ if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
+ Issues.filterResults $dropdown.closest('form')
+ else if $dropdown.hasClass 'js-filter-submit'
+ $dropdown.closest('form').submit()
)
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index b164231e7ef..82532216589 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -627,10 +627,10 @@ class @Notes
if closebtn.text() isnt closetext
closebtn.text(closetext)
- if reopenbtn.is(':not(.btn-comment-and-reopen)')
+ if reopenbtn.is('.btn-comment-and-reopen')
reopenbtn.removeClass('btn-comment-and-reopen')
- if closebtn.is(':not(.btn-comment-and-close)')
+ if closebtn.is('.btn-comment-and-close')
closebtn.removeClass('btn-comment-and-close')
if discardbtn.is(':visible')
diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee
new file mode 100644
index 00000000000..b6b4bd90e6a
--- /dev/null
+++ b/app/assets/javascripts/todos.js.coffee
@@ -0,0 +1,56 @@
+class @Todos
+ constructor: (@name) ->
+ @clearListeners()
+ @initBtnListeners()
+
+ clearListeners: ->
+ $('.done-todo').off('click')
+ $('.js-todos-mark-all').off('click')
+
+ initBtnListeners: ->
+ $('.done-todo').on('click', @doneClicked)
+ $('.js-todos-mark-all').on('click', @allDoneClicked)
+
+ doneClicked: (e) =>
+ e.preventDefault()
+ e.stopImmediatePropagation()
+
+ $this = $(e.currentTarget)
+ $this.disable()
+
+ $.ajax
+ type: 'POST'
+ url: $this.attr('href')
+ dataType: 'json'
+ data: '_method': 'delete'
+ success: (data) =>
+ @clearDone $this.closest('li')
+ @updateBadges data
+
+ allDoneClicked: (e) =>
+ e.preventDefault()
+ e.stopImmediatePropagation()
+
+ $this = $(e.currentTarget)
+ $this.disable()
+
+ $.ajax
+ type: 'POST'
+ url: $this.attr('href')
+ dataType: 'json'
+ data: '_method': 'delete'
+ success: (data) =>
+ $this.remove()
+ $('.js-todos-list').remove()
+ @updateBadges data
+
+ clearDone: ($row) ->
+ $ul = $row.closest('ul')
+ $row.remove()
+
+ if not $ul.find('li').length
+ $ul.parents('.panel').remove()
+
+ updateBadges: (data) ->
+ $('.todos-pending .badge, .todos-pending-count').text data.count
+ $('.todos-done .badge').text data.done_count
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index 987c6f4b8d2..3d6452d2f46 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -4,14 +4,16 @@ class @UsersSelect
@userPath = "/autocomplete/users/:id.json"
$('.js-user-search').each (i, dropdown) =>
- @projectId = $(dropdown).data('project-id')
- @showCurrentUser = $(dropdown).data('current-user')
- showNullUser = $(dropdown).data('null-user')
- showAnyUser = $(dropdown).data('any-user')
- firstUser = $(dropdown).data('first-user')
- selectedId = $(dropdown).data('selected')
-
- $(dropdown).glDropdown(
+ $dropdown = $(dropdown)
+ @projectId = $dropdown.data('project-id')
+ @showCurrentUser = $dropdown.data('current-user')
+ showNullUser = $dropdown.data('null-user')
+ showAnyUser = $dropdown.data('any-user')
+ firstUser = $dropdown.data('first-user')
+ selectedId = $dropdown.data('selected')
+ defaultLabel = $dropdown.data('default-label')
+
+ $dropdown.glDropdown(
data: (term, callback) =>
@users term, (users) =>
if term.length is 0
@@ -52,10 +54,21 @@ class @UsersSelect
search:
fields: ['name', 'username']
selectable: true
- fieldName: $(dropdown).data('field-name')
+ fieldName: $dropdown.data('field-name')
+ toggleLabel: (selected) ->
+ if selected && 'id' of selected
+ selected.name
+ else
+ defaultLabel
clicked: ->
- if $(dropdown).hasClass "js-filter-submit"
- $(dropdown).parents('form').submit()
+ page = $('body').data 'page'
+ isIssueIndex = page is 'projects:issues:index'
+ isMRIndex = page is page is 'projects:merge_requests:index'
+
+ if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
+ Issues.filterResults $dropdown.closest('form')
+ else if $dropdown.hasClass 'js-filter-submit'
+ $dropdown.closest('form').submit()
renderRow: (user) ->
username = if user.username then "@#{user.username}" else ""
avatar = if user.avatar_url then user.avatar_url else false
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index fa115a4bf56..657c5f033c7 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -208,3 +208,13 @@
background-color: #e4e7ed !important;
}
}
+
+.btn-loading {
+ &:not(.disabled) .fa {
+ display: none;
+ }
+
+ .fa {
+ margin-right: 5px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 3197ea84460..a48b6c17fa0 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -9,6 +9,12 @@
border-left: $caret-width-base solid transparent;
}
+.btn-group {
+ .caret {
+ margin-left: 0;
+ }
+}
+
.dropdown {
position: relative;
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index c431e2b0df3..40a508c1ebc 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -3,22 +3,11 @@
vertical-align: top;
}
-@media (min-width: 800px) {
+@media (min-width: $screen-sm-min) {
.issues-filters,
.issues_bulk_update {
- select, .select2-container {
- width: 120px !important;
- display: inline-block;
- }
- }
-}
-
-@media (min-width: 1200px) {
- .issues-filters,
- .issues_bulk_update {
- select, .select2-container {
- width: 150px !important;
- display: inline-block;
+ .dropdown-menu-toggle {
+ width: 132px;
}
}
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index bfec0911b3c..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;
@@ -141,6 +144,10 @@ ul.content-list {
}
}
+.panel > .content-list > li {
+ padding: $gl-padding-top $gl-padding;
+}
+
ul.controls {
padding-top: 1px;
float: right;
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/diff.scss b/app/assets/stylesheets/pages/diff.scss
index db06b8288c2..d5862a11aca 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -1,7 +1,7 @@
// Common
.diff-file {
border: 1px solid $border-color;
- border-top: none;
+ margin-bottom: $gl-padding;
.diff-header {
position: relative;
@@ -361,3 +361,11 @@
border-color: $border;
}
}
+
+.files {
+ margin-top: -1px;
+
+ .diff-file:last-child {
+ margin-bottom: 0;
+ }
+}
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/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 6f93299404c..2760af8a48a 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -1,34 +1,3 @@
-@media (max-width: $screen-sm-max) {
- .issuable-affix {
- margin-top: 20px;
- }
-}
-
-@media (max-width: $screen-md-max) {
- .issuable-affix {
- position: static;
- }
-}
-
-@media (min-width: $screen-md-max) {
- .issuable-affix {
- &.affix-top {
- position: static;
- }
-
- &.affix {
- position: fixed;
- top: 70px;
- margin-right: 35px;
-
- &.no-affix {
- position: relative;
- top: 0;
- }
- }
- }
-}
-
.issuable-details {
section {
.issuable-discussion {
@@ -54,6 +23,10 @@
padding: 6px 10px;
}
}
+
+ &.has-labels {
+ margin-bottom: -5px;
+ }
}
.issuable-sidebar {
@@ -66,8 +39,9 @@
width: $gutter_inner_width;
// --
- &:first-child {
- padding-top: 5px;
+ &.issuable-sidebar-header {
+ padding-top: 0;
+ padding-bottom: 10px;
}
&:last-child {
@@ -75,7 +49,6 @@
}
span {
- margin-top: 7px;
display: inline-block;
}
@@ -84,7 +57,7 @@
}
.issuable-count {
-
+ margin-top: 7px;
}
.gutter-toggle {
@@ -99,19 +72,19 @@
.title {
color: $gl-text-color;
- margin-bottom: 8px;
+ margin-bottom: 10px;
+ line-height: 1;
.avatar {
margin-left: 0;
}
- label {
- font-weight: normal;
- margin-right: 4px;
- }
-
.edit-link {
color: $gl-gray;
+
+ &:hover {
+ color: $md-link-color;
+ }
}
}
@@ -144,11 +117,6 @@
.btn-clipboard {
color: $gl-gray;
}
-
- .participants .avatar {
- margin-top: 6px;
- margin-right: 2px;
- }
}
.right-sidebar {
@@ -163,8 +131,12 @@
&.right-sidebar-expanded {
width: $gutter_width;
- hr {
- display: none;
+ .value {
+ line-height: 1;
+ }
+
+ .bold {
+ font-weight: 600;
}
.sidebar-collapsed-icon {
@@ -172,8 +144,23 @@
}
.gutter-toggle {
+ margin-top: 7px;
border-left: 1px solid $border-gray-light;
}
+
+ .assignee .avatar {
+ float: left;
+ margin-right: 10px;
+ margin-bottom: 0;
+ margin-left: 0;
+ }
+
+ .username {
+ display: block;
+ margin-top: 4px;
+ font-size: 13px;
+ font-weight: normal;
+ }
}
.subscribe-button {
@@ -193,14 +180,6 @@
width: $sidebar_collapsed_width;
padding-top: 0;
- hr {
- margin: 0;
- color: $gray-normal;
- border-color: $gray-normal;
- width: 62px;
- margin-left: -20px
- }
-
.block {
width: $sidebar_collapsed_width - 1px;
margin-left: -19px;
@@ -209,12 +188,18 @@
overflow: hidden;
}
+ .participants {
+ border-bottom: 1px solid $border-gray-light;
+ }
+
.hide-collapsed {
display: none;
}
.gutter-toggle {
- margin-left: -36px;
+ width: 100%;
+ margin-left: 0;
+ padding-left: 25px;
}
.sidebar-collapsed-icon {
@@ -229,6 +214,10 @@
margin-top: 0;
}
+ .author {
+ display: none;
+ }
+
.btn-clipboard {
border: none;
@@ -241,6 +230,11 @@
}
}
}
+
+ .sidebar-collapsed-user {
+ padding-bottom: 0;
+ margin-bottom: 10px;
+ }
}
.btn {
@@ -251,6 +245,13 @@
border: 1px solid $border-gray-dark;
}
}
+
+ a:not(.btn) {
+ &:hover {
+ color: $md-link-color;
+ text-decoration: none;
+ }
+ }
}
.btn-default.gutter-toggle {
@@ -270,3 +271,29 @@
color: $gray-darkest;
}
}
+
+.participants-list {
+ margin: -5px -5px;
+}
+
+.participants-author {
+ display: inline-block;
+ padding: 5px 5px;
+
+ .author_link {
+ display: block;
+ }
+
+ .avatar.avatar-inline {
+ margin: 0;
+ }
+}
+
+.participants-more {
+ margin-top: 5px;
+ margin-left: 5px;
+
+ a {
+ color: #8c8c8c;
+ }
+}
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/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 6c600c99d51..82c5069638d 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -33,6 +33,13 @@
.project-settings-dropdown {
margin-left: 10px;
display: inline-block;
+
+ .dropdown-menu {
+ left: auto;
+ width: auto;
+ right: 0px;
+ max-width: 240px;
+ }
}
}
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 43cf8fa71af..be488483b09 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -1,25 +1,34 @@
class Dashboard::TodosController < Dashboard::ApplicationController
- before_action :find_todos, only: [:index, :destroy_all]
+ before_action :find_todos, only: [:index, :destroy, :destroy_all]
def index
@todos = @todos.page(params[:page]).per(PER_PAGE)
end
def destroy
- todo.done!
+ todo.done
+
+ todo_notice = 'Todo was successfully marked as done.'
respond_to do |format|
- format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' }
+ format.html { redirect_to dashboard_todos_path, notice: todo_notice }
format.js { render nothing: true }
+ format.json do
+ render json: { count: @todos.size, done_count: current_user.todos.done.count }
+ end
end
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.' }
format.js { render nothing: true }
+ format.json do
+ find_todos
+ render json: { count: @todos.size, done_count: current_user.todos.done.count }
+ end
end
end
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/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index aa7a178dcf4..6603f28a082 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -5,7 +5,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :issue, only: [:edit, :update, :show]
# Allow read any issue
- before_action :authorize_read_issue!
+ before_action :authorize_read_issue!, only: [:show]
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
@@ -128,6 +128,10 @@ class Projects::IssuesController < Projects::ApplicationController
end
alias_method :subscribable_resource, :issue
+ def authorize_read_issue!
+ return render_404 unless can?(current_user, :read_issue, @issue)
+ end
+
def authorize_update_issue!
return render_404 unless can?(current_user, :update_issue, @issue)
end
@@ -158,7 +162,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue_params
params.require(:issue).permit(
- :title, :assignee_id, :position, :description,
+ :title, :assignee_id, :position, :description, :confidential,
:milestone_id, :state_event, :task_num, label_ids: []
)
end
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/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 36f37221c58..c9930480770 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -134,7 +134,7 @@ class ProjectsController < ApplicationController
def autocomplete_sources
note_type = params['type']
note_id = params['type_id']
- autocomplete = ::Projects::AutocompleteService.new(@project)
+ autocomplete = ::Projects::AutocompleteService.new(@project, current_user)
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id)
@suggestions = {
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 20a2b0ce8f0..c2befa5a5b3 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -19,4 +19,10 @@ class IssuesFinder < IssuableFinder
def klass
Issue
end
+
+ private
+
+ def init_collection
+ Issue.visible_to_user(current_user)
+ end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 883c2871746..e6ceb213532 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -301,7 +301,7 @@ module ApplicationHelper
if project.nil?
nil
elsif current_controller?(:issues)
- project.issues.send(entity).count
+ project.issues.visible_to_user(current_user).send(entity).count
elsif current_controller?(:merge_requests)
project.merge_requests.send(entity).count
end
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/events_helper.rb b/app/helpers/events_helper.rb
index 37a888d9c60..a67a6b208e2 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -194,7 +194,7 @@ module EventsHelper
end
def event_to_atom(xml, event)
- if event.proper?
+ if event.proper?(current_user)
xml.entry do
event_link = event_feed_url(event)
event_title = event_feed_title(event)
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index ae4ebc0854a..e00d3204027 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -98,6 +98,10 @@ module IssuesHelper
end.sort.to_sentence(last_word_connector: ', or ')
end
+ def confidential_icon(issue)
+ icon('eye-slash') if issue.confidential?
+ end
+
def emoji_icon(name, unicode = nil, aliases = [])
unicode ||= Emoji.emoji_filename(name) rescue ""
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 e8ac8788d9d..c9d8787bd19 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -38,7 +38,7 @@ module MilestonesHelper
def milestone_progress_bar(milestone)
options = {
class: 'progress-bar progress-bar-success',
- style: "width: #{milestone.percent_complete}%;"
+ style: "width: #{milestone.percent_complete(current_user)}%;"
}
content_tag :div, class: 'progress' do
@@ -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/projects_helper.rb b/app/helpers/projects_helper.rb
index b5acb80b720..5473419ef24 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -26,7 +26,7 @@ module ProjectsHelper
image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar]
end
- def link_to_member(project, author, opts = {})
+ def link_to_member(project, author, opts = {}, &block)
default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" }
opts = default_opts.merge(opts)
@@ -44,6 +44,8 @@ module ProjectsHelper
author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name]
end
+ author_html << capture(&block) if block
+
author_html = author_html.html_safe
if opts[:name]
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/ability.rb b/app/models/ability.rb
index ccac08b7d3f..e22da4806e6 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -49,7 +49,6 @@ class Ability
rules = [
:read_project,
:read_wiki,
- :read_issue,
:read_label,
:read_milestone,
:read_project_snippet,
@@ -63,6 +62,9 @@ class Ability
# Allow to read builds by anonymous user if guests are allowed
rules << :read_build if project.public_builds?
+ # Allow to read issues by anonymous user if issue is not confidential
+ rules << :read_issue unless subject.is_a?(Issue) && subject.confidential?
+
rules - project_disabled_features_rules(project)
else
[]
@@ -321,6 +323,7 @@ class Ability
end
rules += project_abilities(user, subject.project)
+ rules = filter_confidential_issues_abilities(user, subject, rules) if subject.is_a?(Issue)
rules
end
end
@@ -439,5 +442,17 @@ class Ability
:"admin_#{name}"
]
end
+
+ def filter_confidential_issues_abilities(user, issue, rules)
+ return rules if user.admin? || !issue.confidential?
+
+ unless issue.author == user || issue.assignee == user || issue.project.team.member?(user.id)
+ rules.delete(:admin_issue)
+ rules.delete(:read_issue)
+ rules.delete(:update_issue)
+ end
+
+ rules
+ end
end
end
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index d67df7c1d9c..5b8e3f654ea 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -1,18 +1,18 @@
module Milestoneish
- def closed_items_count
- issues.closed.size + merge_requests.closed_and_merged.size
+ def closed_items_count(user = nil)
+ issues_visible_to_user(user).closed.size + merge_requests.closed_and_merged.size
end
- def total_items_count
- issues.size + merge_requests.size
+ def total_items_count(user = nil)
+ issues_visible_to_user(user).size + merge_requests.size
end
- def complete?
- total_items_count == closed_items_count
+ def complete?(user = nil)
+ total_items_count(user) == closed_items_count(user)
end
- def percent_complete
- ((closed_items_count * 100) / total_items_count).abs
+ def percent_complete(user = nil)
+ ((closed_items_count(user) * 100) / total_items_count(user)).abs
rescue ZeroDivisionError
0
end
@@ -22,4 +22,8 @@ module Milestoneish
(due_date - Date.today).to_i
end
+
+ def issues_visible_to_user(user = nil)
+ issues.visible_to_user(user)
+ end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 9a0bbf50f8b..a5cfeaf388e 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -73,15 +73,17 @@ class Event < ActiveRecord::Base
end
end
- def proper?
+ def proper?(user = nil)
if push?
true
elsif membership_changed?
true
elsif created_project?
true
+ elsif issue?
+ Ability.abilities.allowed?(user, :read_issue, issue)
else
- ((issue? || merge_request? || note?) && target) || milestone?
+ ((merge_request? || note?) && target) || milestone?
end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 2447f860c5a..5347d4fa1be 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -58,6 +58,13 @@ class Issue < ActiveRecord::Base
attributes
end
+ def self.visible_to_user(user)
+ return where(confidential: false) if user.blank?
+ return all if user.admin?
+
+ where('issues.confidential = false OR (issues.confidential = true AND (issues.author_id = :user_id OR issues.assignee_id = :user_id OR issues.project_id IN(:project_ids)))', user_id: user.id, project_ids: user.authorized_projects.select(:id))
+ end
+
def self.reference_prefix
'#'
end
@@ -99,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
@@ -132,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 188325045e2..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,11 +522,15 @@ 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
- last_commit.try(:sha)
+ last_commit.try(:sha) || source_tip.try(:sha)
+ end
+
+ def source_tip
+ source_branch && source_project.repository.commit(source_branch)
end
def fetch_ref
@@ -568,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/milestone.rb b/app/models/milestone.rb
index 374590ba0c5..de7183bf6b4 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -121,8 +121,8 @@ class Milestone < ActiveRecord::Base
active? && issues.opened.count.zero?
end
- def is_empty?
- total_items_count.zero?
+ def is_empty?(user = nil)
+ total_items_count(user).zero?
end
def author_id
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 036919c27b2..25d24493f6e 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -227,12 +227,6 @@ class Repository
send(key)
end
end
-
- branches.each do |branch|
- unless cache.exist?(:"diverging_commit_counts_#{branch.name}")
- send(:diverging_commit_counts, branch)
- end
- end
end
def expire_tags_cache
@@ -301,18 +295,6 @@ class Repository
@tag_count = nil
end
- def rebuild_cache
- cache_keys.each do |key|
- cache.expire(key)
- send(key)
- end
-
- branches.each do |branch|
- cache.expire(:"diverging_commit_counts_#{branch.name}")
- diverging_commit_counts(branch)
- end
- end
-
def lookup_cache
@lookup_cache ||= {}
end
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/commits/revert_service.rb b/app/services/commits/revert_service.rb
index 9cb918d7a2e..a3c950ede1f 100644
--- a/app/services/commits/revert_service.rb
+++ b/app/services/commits/revert_service.rb
@@ -9,7 +9,8 @@ module Commits
@commit = params[:commit]
@create_merge_request = params[:create_merge_request].present?
- validate and commit
+ check_push_permissions unless @create_merge_request
+ commit
rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError,
ValidationError, ReversionError => ex
error(ex.message)
@@ -45,11 +46,11 @@ module Commits
end
end
- def validate
+ def check_push_permissions
allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch)
unless allowed
- raise_error('You are not allowed to push into this branch')
+ raise ValidationError.new('You are not allowed to push into this branch')
end
true
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/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index 7408e09ed1e..ba50305dbd5 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -1,11 +1,7 @@
module Projects
class AutocompleteService < BaseService
- def initialize(project)
- @project = project
- end
-
def issues
- @project.issues.opened.select([:iid, :title])
+ @project.issues.visible_to_user(current_user).opened.select([:iid, :title])
end
def merge_requests
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/search/global_service.rb b/app/services/search/global_service.rb
index e1e94c5cc38..aa9837038a6 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -11,7 +11,7 @@ module Search
projects = ProjectsFinder.new.execute(current_user)
projects = projects.in_namespace(group.id) if group
- Gitlab::SearchResults.new(projects, params[:search])
+ Gitlab::SearchResults.new(current_user, projects, params[:search])
end
end
end
diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb
index c08881dce4b..4b500914cfb 100644
--- a/app/services/search/project_service.rb
+++ b/app/services/search/project_service.rb
@@ -7,7 +7,8 @@ module Search
end
def execute
- Gitlab::ProjectSearchResults.new(project,
+ Gitlab::ProjectSearchResults.new(current_user,
+ project,
params[:search],
params[:repository_ref])
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 45cfe3da188..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)
@@ -16,7 +16,9 @@
- if todo.pending?
.todo-actions.pull-right
- = link_to 'Done', [:dashboard, todo], method: :delete, class: 'btn'
+ = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do
+ Done
+ = icon('spinner spin')
.todo-body
.todo-note
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 946d7df3933..f9ec3a89158 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -3,13 +3,15 @@
.top-area
%ul.nav-links
- %li{class: ('active' if params[:state].blank? || params[:state] == 'pending')}
+ - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending')
+ %li{class: "todos-pending #{todo_pending_active}"}
= link_to todos_filter_path(state: 'pending') do
%span
To do
%span{class: 'badge'}
= todos_pending_count
- %li{class: ('active' if params[:state] == 'done')}
+ - todo_done_active = ('active' if params[:state] == 'done')
+ %li{class: "todos-done #{todo_done_active}"}
= link_to todos_filter_path(state: 'done') do
%span
Done
@@ -18,7 +20,9 @@
.nav-controls
- if @todos.any?(&:pending?)
- = link_to 'Mark all as done', destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn', method: :delete
+ = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete do
+ Mark all as done
+ = icon('spinner spin')
.todos-filters
.gray-content-block.second-block
@@ -42,12 +46,12 @@
.prepend-top-default
- if @todos.any?
- @todos.group_by(&:project).each do |group|
- .panel.panel-default.panel-small
+ .panel.panel-default.panel-small.js-todos-list
- project = group[0]
.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/events/_event.html.haml b/app/views/events/_event.html.haml
index 36fb2d51629..2d9d9dd6342 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -1,4 +1,4 @@
-- if event.proper?
+- if event.proper?(current_user)
.event-item{class: "#{event.body? ? "event-block" : "event-inline" }"}
.event-item-timestamp
#{time_ago_with_tooltip(event.created_at)}
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/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 0ae83ee01eb..86b46e8c75e 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -67,7 +67,7 @@
%span
Issues
- if @project.default_issues_tracker?
- %span.count.issue_counter= number_with_delimiter(@project.issues.opened.count)
+ %span.count.issue_counter= number_with_delimiter(@project.issues.visible_to_user(current_user).opened.count)
- if project_nav_tab? :merge_requests
= nav_link(controller: :merge_requests) do
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 3ac058a3bf8..3898bb202c5 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -42,13 +42,17 @@
.diff-content.diff-wrap-lines
-# Skipp all non non-supported blobs
- return unless blob.respond_to?('text?')
- - if blob_text_viewable?(blob)
- - if diff_view == 'parallel'
- = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i
- - else
- = 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
+ - if diff_file.too_large?
+ .nothing-here-block
+ This diff could not be displayed because it is too large.
- else
- .nothing-here-block No preview for this file type
+ - if blob_text_viewable?(blob)
+ - if diff_view == 'parallel'
+ = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i
+ - else
+ = 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, 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 a44f34c2a68..4aa92d0b39e 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -3,10 +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
- = link_to_gfm issue.title, issue_path(issue), class: "title"
- %ul.controls.light
+ = confidential_icon(issue)
+ = 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 922a1390355..52df3de8a27 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -22,20 +22,20 @@
= icon('angle-double-left')
.issue-meta
+ = confidential_icon(@issue)
%strong.identifier
Issue ##{@issue.iid}
%span.creator
- by
+ opened
.editor-details
.editor-details
+ = time_ago_with_tooltip(@issue.created_at)
+ by
%strong
= link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-xs")
- %span.hidden-xs
- = '@' + @issue.author.username
%strong
= link_to_member(@project, @issue.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg",
by_username: true, avatar: false)
- = time_ago_with_tooltip(@issue.created_at)
.pull-right.issue-btn-group
- if can?(current_user, :create_issue, @project)
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 a75c0d96c57..eeb605e2dc5 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -8,18 +8,21 @@
= icon('angle-double-left')
.issue-meta
%strong.identifier
- Merge Request ##{@merge_request.iid}
+ %span.hidden-sm.hidden-md.hidden-lg
+ MR
+ %span.hidden-xs
+ Merge Request
+ !#{@merge_request.iid}
%span.creator
- by
+ opened
.editor-details
+ = time_ago_with_tooltip(@merge_request.created_at)
+ by
%strong
= link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-xs")
- %span.hidden-xs
- = '@' + @merge_request.author.username
%strong
= link_to_member(@project, @merge_request.author, size: 24, mobile_classes: "hidden-sm hidden-md hidden-lg",
by_username: true, avatar: false)
- = time_ago_with_tooltip(@merge_request.created_at)
.issue-btn-group.pull-right
- if can?(current_user, :update_merge_request, @merge_request)
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/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index b4597043a27..be63875ab34 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -42,7 +42,7 @@
= preserve do
= markdown @milestone.description
-- if @milestone.complete? && @milestone.active?
+- if @milestone.complete?(current_user) && @milestone.active?
.alert.alert-success.prepend-top-default
%span All issues for this milestone are closed. You may close milestone now.
diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml
index b9486a9b492..24658319060 100644
--- a/app/views/projects/repositories/_download_archive.html.haml
+++ b/app/views/projects/repositories/_download_archive.html.haml
@@ -10,7 +10,7 @@
%span.caret
%span.sr-only
Select Archive Format
- %ul.col-xs-10.dropdown-menu{ role: 'menu' }
+ %ul.col-xs-10.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
%li
= link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), rel: 'nofollow' do
%i.fa.fa-download
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index 45d700781f3..710f5613c81 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -1,5 +1,6 @@
.search-result-row
%h4
+ = confidential_icon(issue)
= link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do
%span.term.str-truncated= issue.title
.pull-right ##{issue.iid}
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
- &nbsp
- %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 d5a4aad05d9..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
@@ -29,6 +38,15 @@
= render 'projects/notes/hints'
.clearfix
.error-alert
+
+- if issuable.is_a?(Issue) && !issuable.project.private?
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :confidential do
+ = f.check_box :confidential
+ This issue is confidential and should only be visible to team members
+
- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
%hr
.form-group
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
+ &nbsp
+ %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/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml
index f1d92ef48b2..3fb409ff727 100644
--- a/app/views/shared/issuable/_participants.html.haml
+++ b/app/views/shared/issuable/_participants.html.haml
@@ -1,3 +1,6 @@
+- participants_row = 7
+- participants_size = participants.size
+- participants_extra = participants_size - participants_row
.block.participants
.sidebar-collapsed-icon
= icon('users')
@@ -5,6 +8,13 @@
= participants.count
.title.hide-collapsed
= pluralize participants.count, "participant"
- - participants.each do |participant|
- %span.hide-collapsed
- = link_to_member(@project, participant, name: false, size: 24)
+ .hide-collapsed.participants-list
+ - participants.each do |participant|
+ .participants-author.js-participants-author
+ = link_to_member(@project, participant, name: false, size: 24)
+ - if participants_extra > 0
+ %div.participants-more
+ %a.js-participants-more{href: "#", data: {original_text: "+ #{participants_size - 7} more", less_text: "- show less"}}
+ + #{participants_extra} more
+:javascript
+ Issue.prototype.PARTICIPANTS_ROW_COUNT = #{participants_row};
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 23b1ed1e51b..2b95b19facc 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,13 +1,12 @@
%aside.right-sidebar{ class: sidebar_gutter_collapsed_class }
.issuable-sidebar
- .block
+ .block.issuable-sidebar-header
%span.issuable-count.hide-collapsed.pull-left
= issuable.iid
of
= issuables_count(issuable)
- %span.pull-right
- %a.gutter-toggle.js-sidebar-toggle{href: '#'}
- = sidebar_gutter_toggle_icon
+ %a.gutter-toggle.pull-right.js-sidebar-toggle{href: '#'}
+ = sidebar_gutter_toggle_icon
.issuable-nav.hide-collapsed.pull-right.btn-group{role: 'group', "aria-label" => '...'}
- if prev_issuable = prev_issuable_for(issuable)
= link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn'
@@ -22,20 +21,20 @@
= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
.block.assignee
- .sidebar-collapsed-icon
+ .sidebar-collapsed-icon.sidebar-collapsed-user{data: {toggle: "tooltip", placement: "left", container: "body"}, title: (issuable.assignee.to_reference if issuable.assignee)}
- if issuable.assignee
- = link_to_member_avatar(issuable.assignee, size: 24)
+ = link_to_member(@project, issuable.assignee, size: 24)
- else
= icon('user')
.title.hide-collapsed
- %label
- Assignee
+ Assignee
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- .pull-right
- = link_to 'Edit', '#', class: 'edit-link'
- .value.hide-collapsed
+ = link_to 'Edit', '#', class: 'edit-link pull-right'
+ .value.bold.hide-collapsed
- if issuable.assignee
- %strong= link_to_member(@project, issuable.assignee, size: 24)
+ = link_to_member(@project, issuable.assignee, size: 32) do
+ %span.username
+ = issuable.assignee.to_reference
- if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee)
%a.pull-right.cannot-be-merged{href: '#', data: {toggle: 'tooltip'}, title: 'Not allowed to merge'}
= icon('exclamation-triangle')
@@ -54,18 +53,13 @@
- else
No
.title.hide-collapsed
- %label
- Milestone
+ Milestone
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- .pull-right
- = link_to 'Edit', '#', class: 'edit-link'
- .value.hide-collapsed
+ = link_to 'Edit', '#', class: 'edit-link pull-right'
+ .value.bold.hide-collapsed
- if issuable.milestone
- %span.back-to-milestone
- = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do
- %strong
- = icon('clock-o')
- = issuable.milestone.title
+ = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do
+ = issuable.milestone.title
- else
.light None
.selectbox.hide-collapsed
@@ -80,11 +74,10 @@
%span
= issuable.labels.count
.title.hide-collapsed
- %label Labels
+ Labels
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- .pull-right
- = link_to 'Edit', '#', class: 'edit-link'
- .value.issuable-show-labels.hide-collapsed
+ = link_to 'Edit', '#', class: 'edit-link pull-right'
+ .value.issuable-show-labels.hide-collapsed{class: ("has-labels" if issuable.labels.any?)}
- if issuable.labels.any?
- issuable.labels.each do |label|
= link_to_label(label, type: issuable.to_ability_name)
@@ -95,14 +88,13 @@
{ selected: issuable.label_ids }, multiple: true, class: 'select2 js-select2', data: { placeholder: "Select labels" }
= render "shared/issuable/participants", participants: issuable.participants(current_user)
- %hr
- if current_user
- subscribed = issuable.subscribed?(current_user)
.block.light.subscription{data: {url: toggle_subscription_path(issuable)}}
.sidebar-collapsed-icon
= icon('rss')
.title.hide-collapsed
- %label.light Notifications
+ Notifications
- subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
%button.btn.btn-block.btn-gray.subscribe-button.hide-collapsed{:type => 'button'}
%span= subscribed ? 'Unsubscribe' : 'Subscribe'
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index f7c6fc14adf..85888096722 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -10,6 +10,8 @@
%strong #{project.name} &middot;
- elsif show_full_project_name
%strong #{project.name_with_namespace} &middot;
+ - if issuable.is_a?(Issue)
+ = confidential_icon(issuable)
= link_to_gfm issuable.title, [project.namespace.becomes(Namespace), project, issuable], title: issuable.title
%div{class: 'issuable-detail'}
= link_to [project.namespace.becomes(Namespace), project, issuable] do
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/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index f01138af3f0..6b25745c554 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -6,10 +6,10 @@
.col-sm-6
%strong= link_to_gfm truncate(milestone.title, length: 100), milestone_path
.col-sm-6
- .pull-right.light #{milestone.percent_complete}% complete
+ .pull-right.light #{milestone.percent_complete(current_user)}% complete
.row
.col-sm-6
- = link_to pluralize(milestone.issues.size, 'Issue'), issues_path
+ = link_to pluralize(milestone.issues_visible_to_user(current_user).size, 'Issue'), issues_path
&middot;
= link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path
.col-sm-6= milestone_progress_bar(milestone)
diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml
index 59d4ae29f79..385c6596606 100644
--- a/app/views/shared/milestones/_summary.html.haml
+++ b/app/views/shared/milestones/_summary.html.haml
@@ -3,15 +3,15 @@
.context.prepend-top-default
.milestone-summary
%h4 Progress
- %strong= milestone.issues.size
+ %strong= milestone.issues_visible_to_user(current_user).size
issues:
%span.milestone-stat
- %strong= milestone.issues.opened.size
+ %strong= milestone.issues_visible_to_user(current_user).opened.size
open and
- %strong= milestone.issues.closed.size
+ %strong= milestone.issues_visible_to_user(current_user).closed.size
closed
%span.milestone-stat
- %strong== #{milestone.percent_complete}%
+ %strong== #{milestone.percent_complete(current_user)}%
complete
%span.milestone-stat
diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml
index 57d7ee85a3b..2b6ce2d7e7a 100644
--- a/app/views/shared/milestones/_tabs.html.haml
+++ b/app/views/shared/milestones/_tabs.html.haml
@@ -2,7 +2,7 @@
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
Issues
- %span.badge= milestone.issues.size
+ %span.badge= milestone.issues_visible_to_user(current_user).size
%li
= link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
Merge Requests
@@ -21,7 +21,7 @@
.tab-content.milestone-content
.tab-pane.active#tab-issues
- = render 'shared/milestones/issues_tab', issues: milestone.issues, show_project_name: show_project_name, show_full_project_name: show_full_project_name
+ = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user), show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane#tab-merge-requests
= render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane#tab-participants
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 4cf1d948b5b..cab8743a077 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -28,7 +28,7 @@
%h2.title
= markdown escape_once(milestone.title), pipeline: :single_line
-- if milestone.complete? && milestone.active?
+- if milestone.complete?(current_user) && milestone.active?
.alert.alert-success.prepend-top-default
- close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.'
%span All issues for this milestone are closed. #{close_msg}
@@ -47,7 +47,7 @@
- project_name = group ? ms.project.name : ms.project.name_with_namespace
= link_to project_name, namespace_project_milestone_path(ms.project.namespace, ms.project, ms)
%td
- = ms.issues.opened.count
+ = ms.issues_visible_to_user(current_user).opened.count
%td
- if ms.closed?
Closed
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/20160223192159_add_confidential_to_issues.rb b/db/migrate/20160223192159_add_confidential_to_issues.rb
new file mode 100644
index 00000000000..e9d47fd589a
--- /dev/null
+++ b/db/migrate/20160223192159_add_confidential_to_issues.rb
@@ -0,0 +1,6 @@
+class AddConfidentialToIssues < ActiveRecord::Migration
+ def change
+ add_column :issues, :confidential, :boolean, default: false
+ add_index :issues, :confidential
+ end
+end
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 2f075677b30..5b2f5aa3ddd 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160316123110) do
+ActiveRecord::Schema.define(version: 20160316204731) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -416,10 +416,12 @@ ActiveRecord::Schema.define(version: 20160316123110) do
t.string "state"
t.integer "iid"
t.integer "updated_by_id"
+ t.boolean "confidential", default: false
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
+ add_index "issues", ["confidential"], name: "index_issues_on_confidential", using: :btree
add_index "issues", ["created_at", "id"], name: "index_issues_on_created_at_and_id", using: :btree
add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree
add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
@@ -865,7 +867,7 @@ ActiveRecord::Schema.define(version: 20160316123110) 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
@@ -873,9 +875,11 @@ ActiveRecord::Schema.define(version: 20160316123110) 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/README.md b/doc/README.md
index db19c3de8d1..08d0a6a5bfb 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -3,6 +3,7 @@
## User documentation
- [API](api/README.md) Automate GitLab via a simple and powerful API.
+- [CI](ci/README.md)
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
- [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
- [Importing to GitLab](workflow/importing/README.md).
@@ -16,42 +17,6 @@
- [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project.
- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN.
-## CI User documentation
-
-- [Get started with GitLab CI](ci/quick_start/README.md)
-- [Learn how to enable or disable GitLab CI](ci/enable_or_disable_ci.md)
-- [Learn how `.gitlab-ci.yml` works](ci/yaml/README.md)
-- [Configure a Runner, the application that runs your builds](ci/runners/README.md)
-- [Use Docker images with GitLab Runner](ci/docker/using_docker_images.md)
-- [Use CI to build Docker images](ci/docker/using_docker_build.md)
-- [Use variables in your `.gitlab-ci.yml`](ci/variables/README.md)
-- [Use SSH keys in your build environment](ci/ssh_keys/README.md)
-- [Trigger builds through the API](ci/triggers/README.md)
-- [Build artifacts](ci/build_artifacts/README.md)
-- [User permissions](ci/permissions/README.md)
-- [API](ci/api/README.md)
-
-### CI Examples
-
-- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
-- [Test your PHP applications](ci/examples/php.md)
-- [Test and deploy Ruby applications to Heroku](ci/examples/test-and-deploy-ruby-application-to-heroku.md)
-- [Test and deploy Python applications to Heroku](ci/examples/test-and-deploy-python-application-to-heroku.md)
-- [Test Clojure applications](ci/examples/test-clojure-application.md)
-- [Using `dpl` as deployment tool](ci/deployment/README.md)
-- Help your favorite programming language and GitLab by sending a merge request
- with a guide for that language.
-
-### CI Services
-
-GitLab CI uses the `services` keyword to define what docker containers should
-be linked with your base image. Below is a list of examples you may use:
-
-- [Using MySQL](ci/services/mysql.md)
-- [Using PostgreSQL](ci/services/postgres.md)
-- [Using Redis](ci/services/redis.md)
-- [Using Other Services](ci/docker/using_docker_images.md#how-to-use-other-images-as-services)
-
## Administrator documentation
- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when webhooks aren't enough.
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/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md
index 15051dd76f9..dcdf49d3379 100644
--- a/doc/hooks/custom_hooks.md
+++ b/doc/hooks/custom_hooks.md
@@ -2,7 +2,7 @@
**Note: Custom git hooks must be configured on the filesystem of the GitLab
server. Only GitLab server administrators will be able to complete these tasks.
-Please explore [webhooks](doc/web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).**
+Please explore [webhooks](../web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).**
Git natively supports hooks that are executed on different actions.
Examples of server-side git hooks include pre-receive, post-receive, and update.
diff --git a/doc/install/installation.md b/doc/install/installation.md
index aa989417c4b..c567846f624 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -76,7 +76,7 @@ Make sure you have the right version of Git installed
# Install Git
sudo apt-get install -y git-core
- # Make sure Git is version 2.7.3 or higher
+ # Make sure Git is version 2.7.4 or higher
git --version
Is the system packaged Git too old? Remove it and compile from source.
@@ -89,9 +89,9 @@ Is the system packaged Git too old? Remove it and compile from source.
# Download and compile from source
cd /tmp
- curl -O --progress https://www.kernel.org/pub/software/scm/git/git-2.7.3.tar.gz
- echo '30d067499b61caddedaf1a407b4947244f14d10842d100f7c7c6ea1c288280cd git-2.7.3.tar.gz' | shasum -a256 -c - && tar -xzf git-2.7.3.tar.gz
- cd git-2.7.3/
+ curl -O --progress https://www.kernel.org/pub/software/scm/git/git-2.7.4.tar.gz
+ echo '7104c4f5d948a75b499a954524cb281fe30c6649d8abe20982936f75ec1f275b git-2.7.4.tar.gz' | shasum -a256 -c - && tar -xzf git-2.7.4.tar.gz
+ cd git-2.7.4/
./configure
make prefix=/usr/local all
@@ -161,7 +161,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
# Install the database packages
sudo apt-get install -y postgresql postgresql-client libpq-dev
-
+
# Create a user for GitLab
sudo -u postgres psql -d template1 -c "CREATE USER git CREATEDB;"
diff --git a/doc/release/security.md b/doc/release/security.md
index b1a62b333e6..118c016ba4f 100644
--- a/doc/release/security.md
+++ b/doc/release/security.md
@@ -15,7 +15,7 @@ Please report suspected security vulnerabilities in private to <support@gitlab.c
1. Verify that the issue can be reproduced
1. Acknowledge the issue to the researcher that disclosed it
1. Inform the release manager that there needs to be a security release
-1. Do the steps from [patch release document](doc/release/patch.md), starting with "Create an issue on private GitLab development server"
+1. Do the steps from [patch release document](../release/patch.md), starting with "Create an issue on private GitLab development server"
1. The MR with the security fix should get a 'security' label and be assigned to the release manager
1. Build the package for GitLab.com and do a deploy
1. Build the package for ci.gitLab.com and do a deploy
diff --git a/doc/security/two_factor_authentication.md b/doc/security/two_factor_authentication.md
index 8365bdb7b1b..c8499380c18 100644
--- a/doc/security/two_factor_authentication.md
+++ b/doc/security/two_factor_authentication.md
@@ -6,7 +6,7 @@ password to login, they'll be prompted for a code generated by an application on
their phone.
You can read more about it here:
-[Two-factor Authentication (2FA)](doc/profile/two_factor_authentication.md)
+[Two-factor Authentication (2FA)](../profile/two_factor_authentication.md)
## Enabling 2FA
diff --git a/doc/update/8.2-to-8.3.md b/doc/update/8.2-to-8.3.md
index 2ca4e1f3770..9f5c6c4dc84 100644
--- a/doc/update/8.2-to-8.3.md
+++ b/doc/update/8.2-to-8.3.md
@@ -1,5 +1,14 @@
# From 8.2 to 8.3
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
**NOTE:** GitLab 8.0 introduced several significant changes related to
installation and configuration which *are not duplicated here*. Be sure you're
already running a working version of at least 8.0 before proceeding with this
diff --git a/doc/update/8.3-to-8.4.md b/doc/update/8.3-to-8.4.md
index 269deec7a9c..9f6517d9487 100644
--- a/doc/update/8.3-to-8.4.md
+++ b/doc/update/8.3-to-8.4.md
@@ -1,5 +1,14 @@
# From 8.3 to 8.4
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
### 1. Stop server
sudo service gitlab stop
diff --git a/doc/update/8.4-to-8.5.md b/doc/update/8.4-to-8.5.md
index 0a9cb5683e7..0cb137a03cc 100644
--- a/doc/update/8.4-to-8.5.md
+++ b/doc/update/8.4-to-8.5.md
@@ -1,5 +1,14 @@
# From 8.4 to 8.5
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
### 1. Stop server
sudo service gitlab stop
diff --git a/doc/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md
index 024f6e8a433..7d63915af5e 100644
--- a/doc/update/8.5-to-8.6.md
+++ b/doc/update/8.5-to-8.6.md
@@ -1,5 +1,14 @@
# From 8.5 to 8.6
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
### 1. Stop server
sudo service gitlab stop
diff --git a/doc/workflow/importing/import_projects_from_bitbucket.md b/doc/workflow/importing/import_projects_from_bitbucket.md
index 1e9825e2e10..520c4216295 100644
--- a/doc/workflow/importing/import_projects_from_bitbucket.md
+++ b/doc/workflow/importing/import_projects_from_bitbucket.md
@@ -1,6 +1,6 @@
# Import your project from Bitbucket to GitLab
-It takes just a few steps to import your existing Bitbucket projects to GitLab. But keep in mind that it is possible only if Bitbucket support is enabled on your GitLab instance. You can read more about Bitbucket support [here](doc/integration/bitbucket.md).
+It takes just a few steps to import your existing Bitbucket projects to GitLab. But keep in mind that it is possible only if Bitbucket support is enabled on your GitLab instance. You can read more about Bitbucket support [here](../../integration/bitbucket.md).
* Sign in to GitLab.com and go to your dashboard
diff --git a/doc/workflow/protected_branches.md b/doc/workflow/protected_branches.md
index fdf9a8d391c..d854ec1e025 100644
--- a/doc/workflow/protected_branches.md
+++ b/doc/workflow/protected_branches.md
@@ -12,7 +12,7 @@ A protected branch does three simple things:
You can make any branch a protected branch. GitLab makes the master branch a protected branch by default.
-To protect a branch, user needs to have at least a Master permission level, see [permissions document](doc/permissions/permissions.md).
+To protect a branch, user needs to have at least a Master permission level, see [permissions document](../permissions/permissions.md).
![protected branches page](protected_branches/protected_branches1.png)
diff --git a/features/project/issues/award_emoji.feature b/features/project/issues/award_emoji.feature
index 2945bb3753a..f0fd414a9f9 100644
--- a/features/project/issues/award_emoji.feature
+++ b/features/project/issues/award_emoji.feature
@@ -18,21 +18,24 @@ Feature: Award Emoji
@javascript
Scenario: I add and remove custom award in the issue
Given I click to emoji-picker
- Then The search field is focused
- And I click to emoji in the picker
+ Then The emoji menu is visible
+ And The search field is focused
+ Then I click to emoji in the picker
Then I have award added
And I can remove it by clicking to icon
@javascript
Scenario: I can see the list of emoji categories
Given I click to emoji-picker
- Then The search field is focused
+ Then The emoji menu is visible
+ And The search field is focused
Then I can see the activity and food categories
@javascript
Scenario: I can search emoji
Given I click to emoji-picker
- Then The search field is focused
+ Then The emoji menu is visible
+ And The search field is focused
And I search "hand"
Then I see search result for "hand"
diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature
index 74685d24a7d..823658b4f24 100644
--- a/features/project/merge_requests.feature
+++ b/features/project/merge_requests.feature
@@ -325,3 +325,11 @@ Feature: Project Merge Requests
When I click the "Target branch" dropdown
And I select a new target branch
Then I should see new target branch changes
+
+ @javascript
+ Scenario: I can close merge request after commenting
+ Given I visit merge request page "Bug NS-04"
+ And I leave a comment like "XML attached"
+ Then I should see comment "XML attached"
+ And I click link "Close"
+ Then I should see closed merge request "Bug NS-04"
diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb
index 9722a5a848c..963e4f21365 100644
--- a/features/steps/dashboard/todos.rb
+++ b/features/steps/dashboard/todos.rb
@@ -41,7 +41,6 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
click_link 'Done'
end
- expect(page).to have_content 'Todo was successfully marked as done.'
expect(page).to have_content 'To do 3'
expect(page).to have_content 'Done 1'
should_not_see_todo "John Doe assigned you merge request !#{merge_request.iid}"
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
index 7a6ae15ffa5..e5b7db4c5e3 100644
--- a/features/steps/groups.rb
+++ b/features/steps/groups.rb
@@ -35,7 +35,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
end
step 'I should see projects activity feed' do
- expect(page).to have_content 'closed issue'
+ expect(page).to have_content 'joined project'
end
step 'I should see issues from group "Owned" assigned to me' do
diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb
index ce2554bc80d..c5d45709b44 100644
--- a/features/steps/project/issues/award_emoji.rb
+++ b/features/steps/project/issues/award_emoji.rb
@@ -92,6 +92,10 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
end
end
+ step 'The emoji menu is visible' do
+ page.find(".emoji-menu.is-visible")
+ end
+
step 'The search field is focused' do
expect(page).to have_selector('#emoji_search')
expect(page.evaluate_script('document.activeElement.id')).to eq('emoji_search')
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 252744515da..fda6f841438 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -82,7 +82,7 @@ module API
# GET /projects/:id/issues?milestone=1.0.0&state=closed
# GET /issues?iid=42
get ":id/issues" do
- issues = user_project.issues
+ issues = user_project.issues.visible_to_user(current_user)
issues = filter_issues_state(issues, params[:state]) unless params[:state].nil?
issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil?
@@ -104,6 +104,7 @@ module API
# GET /projects/:id/issues/:issue_id
get ":id/issues/:issue_id" do
@issue = user_project.issues.find(params[:issue_id])
+ not_found! unless can?(current_user, :read_issue, @issue)
present @issue, with: Entities::Issue
end
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index 9f08aa36e8b..2732e0b5145 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -9,6 +9,11 @@ module Banzai
Issue
end
+ def self.user_can_see_reference?(user, node, context)
+ issue = Issue.find(node.attr('data-issue')) rescue nil
+ Ability.abilities.allowed?(user, :read_issue, issue)
+ end
+
def find_object(project, id)
project.get_issue(id)
end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index faa2830c16e..d2e85cabf72 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -24,6 +24,10 @@ module Gitlab
@lines ||= parser.parse(raw_diff.each_line).to_a
end
+ def too_large?
+ diff.too_large?
+ end
+
def highlighted_diff_lines
Gitlab::Diff::Highlight.new(self).highlight
end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 0607a8b9592..71c5b6801fb 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -2,7 +2,8 @@ module Gitlab
class ProjectSearchResults < SearchResults
attr_reader :project, :repository_ref
- def initialize(project, query, repository_ref = nil)
+ def initialize(current_user, project, query, repository_ref = nil)
+ @current_user = current_user
@project = project
@repository_ref = if repository_ref.present?
repository_ref
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index f13528a2eea..f8ab2b1f09e 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -1,12 +1,13 @@
module Gitlab
class SearchResults
- attr_reader :query
+ attr_reader :current_user, :query
# Limit search results by passed projects
# It allows us to search only for projects user has access to
attr_reader :limit_projects
- def initialize(limit_projects, query)
+ def initialize(current_user, limit_projects, query)
+ @current_user = current_user
@limit_projects = limit_projects || Project.all
@query = Shellwords.shellescape(query) if query.present?
end
@@ -58,7 +59,7 @@ module Gitlab
end
def issues
- issues = Issue.where(project_id: project_ids_relation)
+ issues = Issue.visible_to_user(current_user).where(project_id: project_ids_relation)
if query =~ /#(\d+)\z/
issues = issues.where(iid: $1)
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 76d56bc989d..2cd81231144 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -1,16 +1,16 @@
require('spec_helper')
describe Projects::IssuesController do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
- let(:issue) { create(:issue, project: project) }
+ describe "GET #index" do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, project: project) }
- before do
- sign_in(user)
- project.team << [user, :developer]
- end
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
- describe "GET #index" do
it "returns index" do
get :index, namespace_id: project.namespace.path, project_id: project.path
@@ -38,6 +38,152 @@ describe Projects::IssuesController do
get :index, namespace_id: project.namespace.path, project_id: project.path
expect(response.status).to eq(404)
end
+ end
+
+ describe 'Confidential Issues' do
+ let(:project) { create(:empty_project, :public) }
+ let(:assignee) { create(:assignee) }
+ let(:author) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let!(:issue) { create(:issue, project: project) }
+ let!(:unescaped_parameter_value) { create(:issue, :confidential, project: project, author: author) }
+ let!(:request_forgery_timing_attack) { create(:issue, :confidential, project: project, assignee: assignee) }
+
+ describe 'GET #index' do
+ it 'should not list confidential issues for guests' do
+ sign_out(:user)
+ get_issues
+
+ expect(assigns(:issues)).to eq [issue]
+ end
+
+ it 'should not list confidential issues for non project members' do
+ sign_in(non_member)
+ get_issues
+
+ expect(assigns(:issues)).to eq [issue]
+ end
+
+ it 'should list confidential issues for author' do
+ sign_in(author)
+ get_issues
+
+ expect(assigns(:issues)).to include unescaped_parameter_value
+ expect(assigns(:issues)).not_to include request_forgery_timing_attack
+ end
+
+ it 'should list confidential issues for assignee' do
+ sign_in(assignee)
+ get_issues
+
+ expect(assigns(:issues)).not_to include unescaped_parameter_value
+ expect(assigns(:issues)).to include request_forgery_timing_attack
+ end
+
+ it 'should list confidential issues for project members' do
+ sign_in(member)
+ project.team << [member, :developer]
+
+ get_issues
+
+ expect(assigns(:issues)).to include unescaped_parameter_value
+ expect(assigns(:issues)).to include request_forgery_timing_attack
+ end
+
+ it 'should list confidential issues for admin' do
+ sign_in(admin)
+ get_issues
+
+ expect(assigns(:issues)).to include unescaped_parameter_value
+ expect(assigns(:issues)).to include request_forgery_timing_attack
+ end
+
+ def get_issues
+ get :index,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param
+ end
+ end
+ shared_examples_for 'restricted action' do |http_status|
+ it 'returns 404 for guests' do
+ sign_out :user
+ go(id: unescaped_parameter_value.to_param)
+
+ expect(response).to have_http_status :not_found
+ end
+
+ it 'returns 404 for non project members' do
+ sign_in(non_member)
+ go(id: unescaped_parameter_value.to_param)
+
+ expect(response).to have_http_status :not_found
+ end
+
+ it "returns #{http_status[:success]} for author" do
+ sign_in(author)
+ go(id: unescaped_parameter_value.to_param)
+
+ expect(response).to have_http_status http_status[:success]
+ end
+
+ it "returns #{http_status[:success]} for assignee" do
+ sign_in(assignee)
+ go(id: request_forgery_timing_attack.to_param)
+
+ expect(response).to have_http_status http_status[:success]
+ end
+
+ it "returns #{http_status[:success]} for project members" do
+ sign_in(member)
+ project.team << [member, :developer]
+ go(id: unescaped_parameter_value.to_param)
+
+ expect(response).to have_http_status http_status[:success]
+ end
+
+ it "returns #{http_status[:success]} for admin" do
+ sign_in(admin)
+ go(id: unescaped_parameter_value.to_param)
+
+ expect(response).to have_http_status http_status[:success]
+ end
+ end
+
+ describe 'GET #show' do
+ it_behaves_like 'restricted action', success: 200
+
+ def go(id:)
+ get :show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: id
+ end
+ end
+
+ describe 'GET #edit' do
+ it_behaves_like 'restricted action', success: 200
+
+ def go(id:)
+ get :edit,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: id
+ end
+ end
+
+ describe 'PUT #update' do
+ it_behaves_like 'restricted action', success: 302
+
+ def go(id:)
+ put :update,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: id,
+ issue: { title: 'New title' }
+ end
+ end
end
end
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index 722095de590..e72aa9479b7 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -4,6 +4,10 @@ FactoryGirl.define do
author
project
+ trait :confidential do
+ confidential true
+ end
+
trait :closed do
state :closed
end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index a9df5fa1d3a..e281e2f227b 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -51,6 +51,11 @@ FactoryGirl.define do
trait :with_diffs do
end
+ trait :without_diffs do
+ source_branch "improve/awesome"
+ target_branch "master"
+ end
+
trait :conflict do
source_branch "feature_conflict"
target_branch "feature"
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/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb
index 1f3bd915f48..9219b767547 100644
--- a/spec/features/issues/new_branch_button_spec.rb
+++ b/spec/features/issues/new_branch_button_spec.rb
@@ -24,7 +24,7 @@ feature 'Start new branch from an issue', feature: true do
end
let(:referenced_mr) do
create(:merge_request, :simple, source_project: project, target_project: project,
- description: "Fixes ##{issue.iid}")
+ description: "Fixes ##{issue.iid}", author: user)
end
before do
diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index 1b2fd1bab10..b76e4c74c79 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -30,8 +30,6 @@ feature 'Merge Request filtering by Milestone', feature: true do
def filter_by_milestone(title)
find(".js-milestone-select").click
- sleep 0.5
find(".milestone-filter a", text: title).click
- sleep 1
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/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb
index e9bb388e361..9acf6304bcb 100644
--- a/spec/lib/banzai/filter/redactor_filter_spec.rb
+++ b/spec/lib/banzai/filter/redactor_filter_spec.rb
@@ -44,8 +44,78 @@ describe Banzai::Filter::RedactorFilter, lib: true do
end
end
- context "for user references" do
+ context 'with data-issue' do
+ context 'for confidential issues' do
+ it 'removes references for non project members' do
+ non_member = create(:user)
+ project = create(:empty_project, :public)
+ issue = create(:issue, :confidential, project: project)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ doc = filter(link, current_user: non_member)
+
+ expect(doc.css('a').length).to eq 0
+ end
+
+ it 'allows references for author' do
+ author = create(:user)
+ project = create(:empty_project, :public)
+ issue = create(:issue, :confidential, project: project, author: author)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ doc = filter(link, current_user: author)
+
+ expect(doc.css('a').length).to eq 1
+ end
+
+ it 'allows references for assignee' do
+ assignee = create(:user)
+ project = create(:empty_project, :public)
+ issue = create(:issue, :confidential, project: project, assignee: assignee)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ doc = filter(link, current_user: assignee)
+ expect(doc.css('a').length).to eq 1
+ end
+
+ it 'allows references for project members' do
+ member = create(:user)
+ project = create(:empty_project, :public)
+ project.team << [member, :developer]
+ issue = create(:issue, :confidential, project: project)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ doc = filter(link, current_user: member)
+
+ expect(doc.css('a').length).to eq 1
+ end
+
+ it 'allows references for admin' do
+ admin = create(:admin)
+ project = create(:empty_project, :public)
+ issue = create(:issue, :confidential, project: project)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ doc = filter(link, current_user: admin)
+
+ expect(doc.css('a').length).to eq 1
+ end
+ end
+
+ it 'allows references for non confidential issues' do
+ user = create(:user)
+ project = create(:empty_project, :public)
+ issue = create(:issue, project: project)
+
+ link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter')
+ doc = filter(link, current_user: user)
+
+ expect(doc.css('a').length).to eq 1
+ end
+ end
+
+ context "for user references" do
context 'with data-group' do
it 'removes unpermitted Group references' do
user = create(:user)
diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb
index 04cf11fc6f1..844fd79c991 100644
--- a/spec/lib/gitlab/closing_issue_extractor_spec.rb
+++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb
@@ -11,6 +11,7 @@ describe Gitlab::ClosingIssueExtractor, lib: true do
subject { described_class.new(project, project.creator) }
before do
+ project.team << [project.creator, :developer]
project2.team << [project.creator, :master]
end
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 0d9694f2c13..a0cbef6e6a4 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -18,4 +18,18 @@ describe Gitlab::Diff::File, lib: true do
describe :mode_changed? do
it { expect(diff_file.mode_changed?).to be_falsey }
end
+
+ describe '#too_large?' do
+ it 'returns true for a file that is too large' do
+ expect(diff).to receive(:too_large?).and_return(true)
+
+ expect(diff_file.too_large?).to eq(true)
+ end
+
+ it 'returns false for a file that is small enough' do
+ expect(diff).to receive(:too_large?).and_return(false)
+
+ expect(diff_file.too_large?).to eq(false)
+ end
+ end
end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 09adbc07dcb..db0ff95b4f5 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -1,11 +1,12 @@
require 'spec_helper'
describe Gitlab::ProjectSearchResults, lib: true do
+ let(:user) { create(:user) }
let(:project) { create(:project) }
let(:query) { 'hello world' }
describe 'initialize with empty ref' do
- let(:results) { Gitlab::ProjectSearchResults.new(project, query, '') }
+ let(:results) { Gitlab::ProjectSearchResults.new(user, project, query, '') }
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to be_nil }
@@ -14,10 +15,74 @@ describe Gitlab::ProjectSearchResults, lib: true do
describe 'initialize with ref' do
let(:ref) { 'refs/heads/test' }
- let(:results) { Gitlab::ProjectSearchResults.new(project, query, ref) }
+ let(:results) { Gitlab::ProjectSearchResults.new(user, project, query, ref) }
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to eq(ref) }
it { expect(results.query).to eq('hello world') }
end
+
+ describe 'confidential issues' do
+ let(:query) { 'issue' }
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let!(:issue) { create(:issue, project: project, title: 'Issue 1') }
+ let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
+ let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) }
+
+ it 'should not list project confidential issues for non project members' do
+ results = described_class.new(non_member, project, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).not_to include security_issue_1
+ expect(issues).not_to include security_issue_2
+ expect(results.issues_count).to eq 1
+ end
+
+ it 'should list project confidential issues for author' do
+ results = described_class.new(author, project, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).to include security_issue_1
+ expect(issues).not_to include security_issue_2
+ expect(results.issues_count).to eq 2
+ end
+
+ it 'should list project confidential issues for assignee' do
+ results = described_class.new(assignee, project.id, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).not_to include security_issue_1
+ expect(issues).to include security_issue_2
+ expect(results.issues_count).to eq 2
+ end
+
+ it 'should list project confidential issues for project members' do
+ project.team << [member, :developer]
+
+ results = described_class.new(member, project, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).to include security_issue_1
+ expect(issues).to include security_issue_2
+ expect(results.issues_count).to eq 3
+ end
+
+ it 'should list all project issues for admin' do
+ results = described_class.new(admin, project, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).to include security_issue_1
+ expect(issues).to include security_issue_2
+ expect(results.issues_count).to eq 3
+ end
+ end
end
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 7d963795e17..65af37e24f1 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe Gitlab::ReferenceExtractor, lib: true do
let(:project) { create(:project) }
+
subject { Gitlab::ReferenceExtractor.new(project, project.creator) }
it 'accesses valid user objects' do
@@ -41,6 +42,7 @@ describe Gitlab::ReferenceExtractor, lib: true do
end
it 'accesses valid issue objects' do
+ project.team << [project.creator, :developer]
@i0 = create(:issue, project: project)
@i1 = create(:issue, project: project)
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index bb18f417858..f4afe597e8d 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe Gitlab::SearchResults do
+ let(:user) { create(:user) }
let!(:project) { create(:project, name: 'foo') }
let!(:issue) { create(:issue, project: project, title: 'foo') }
@@ -9,7 +10,7 @@ describe Gitlab::SearchResults do
end
let!(:milestone) { create(:milestone, project: project, title: 'foo') }
- let(:results) { described_class.new(Project.all, 'foo') }
+ let(:results) { described_class.new(user, Project.all, 'foo') }
describe '#total_count' do
it 'returns the total amount of search hits' do
@@ -52,4 +53,92 @@ describe Gitlab::SearchResults do
expect(results.empty?).to eq(false)
end
end
+
+ describe 'confidential issues' do
+ let(:project_1) { create(:empty_project) }
+ let(:project_2) { create(:empty_project) }
+ let(:project_3) { create(:empty_project) }
+ let(:project_4) { create(:empty_project) }
+ let(:query) { 'issue' }
+ let(:limit_projects) { Project.where(id: [project_1.id, project_2.id, project_3.id]) }
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let!(:issue) { create(:issue, project: project_1, title: 'Issue 1') }
+ let!(:security_issue_1) { create(:issue, :confidential, project: project_1, title: 'Security issue 1', author: author) }
+ let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project_1, assignee: assignee) }
+ let!(:security_issue_3) { create(:issue, :confidential, project: project_2, title: 'Security issue 3', author: author) }
+ let!(:security_issue_4) { create(:issue, :confidential, project: project_3, title: 'Security issue 4', assignee: assignee) }
+ let!(:security_issue_5) { create(:issue, :confidential, project: project_4, title: 'Security issue 5') }
+
+ it 'should not list confidential issues for non project members' do
+ results = described_class.new(non_member, limit_projects, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).not_to include security_issue_1
+ expect(issues).not_to include security_issue_2
+ expect(issues).not_to include security_issue_3
+ expect(issues).not_to include security_issue_4
+ expect(issues).not_to include security_issue_5
+ expect(results.issues_count).to eq 1
+ end
+
+ it 'should list confidential issues for author' do
+ results = described_class.new(author, limit_projects, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).to include security_issue_1
+ expect(issues).not_to include security_issue_2
+ expect(issues).to include security_issue_3
+ expect(issues).not_to include security_issue_4
+ expect(issues).not_to include security_issue_5
+ expect(results.issues_count).to eq 3
+ end
+
+ it 'should list confidential issues for assignee' do
+ results = described_class.new(assignee, limit_projects, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).not_to include security_issue_1
+ expect(issues).to include security_issue_2
+ expect(issues).not_to include security_issue_3
+ expect(issues).to include security_issue_4
+ expect(issues).not_to include security_issue_5
+ expect(results.issues_count).to eq 3
+ end
+
+ it 'should list confidential issues for project members' do
+ project_1.team << [member, :developer]
+ project_2.team << [member, :developer]
+
+ results = described_class.new(member, limit_projects, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).to include security_issue_1
+ expect(issues).to include security_issue_2
+ expect(issues).to include security_issue_3
+ expect(issues).not_to include security_issue_4
+ expect(issues).not_to include security_issue_5
+ expect(results.issues_count).to eq 4
+ end
+
+ it 'should list all issues for admin' do
+ results = described_class.new(admin, limit_projects, query)
+ issues = results.objects('issues')
+
+ expect(issues).to include issue
+ expect(issues).to include security_issue_1
+ expect(issues).to include security_issue_2
+ expect(issues).to include security_issue_3
+ expect(issues).to include security_issue_4
+ expect(issues).not_to include security_issue_5
+ expect(results.issues_count).to eq 5
+ end
+ end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 253902512c3..0e9111c8029 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -86,10 +86,21 @@ eos
let(:issue) { create :issue, project: project }
let(:other_project) { create :project, :public }
let(:other_issue) { create :issue, project: other_project }
+ let(:commiter) { create :user }
+
+ before do
+ project.team << [commiter, :developer]
+ other_project.team << [commiter, :developer]
+ end
it 'detects issues that this commit is marked as closing' do
ext_ref = "#{other_project.path_with_namespace}##{other_issue.iid}"
- allow(commit).to receive(:safe_message).and_return("Fixes ##{issue.iid} and #{ext_ref}")
+
+ allow(commit).to receive_messages(
+ safe_message: "Fixes ##{issue.iid} and #{ext_ref}",
+ committer_email: commiter.email
+ )
+
expect(commit.closes_issues).to include(issue)
expect(commit.closes_issues).to include(other_issue)
end
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index 20f0c561e44..cb33edde820 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -48,7 +48,8 @@ describe Issue, "Mentionable" do
describe '#create_new_cross_references!' do
let(:project) { create(:project) }
- let(:issues) { create_list(:issue, 2, project: project) }
+ let(:author) { create(:author) }
+ let(:issues) { create_list(:issue, 2, project: project, author: author) }
context 'before changes are persisted' do
it 'ignores pre-existing references' do
@@ -91,7 +92,7 @@ describe Issue, "Mentionable" do
end
def create_issue(description:)
- create(:issue, project: project, description: description)
+ create(:issue, project: project, description: description, author: author)
end
end
end
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
new file mode 100644
index 00000000000..47c3be673c5
--- /dev/null
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -0,0 +1,104 @@
+require 'spec_helper'
+
+describe Milestone, 'Milestoneish' do
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:project) { create(:project, :public) }
+ let(:milestone) { create(:milestone, project: project) }
+ let!(:issue) { create(:issue, project: project, milestone: milestone) }
+ let!(:security_issue_1) { create(:issue, :confidential, project: project, author: author, milestone: milestone) }
+ let!(:security_issue_2) { create(:issue, :confidential, project: project, assignee: assignee, milestone: milestone) }
+ let!(:closed_issue_1) { create(:issue, :closed, project: project, milestone: milestone) }
+ let!(:closed_issue_2) { create(:issue, :closed, project: project, milestone: milestone) }
+ let!(:closed_security_issue_1) { create(:issue, :confidential, :closed, project: project, author: author, milestone: milestone) }
+ let!(:closed_security_issue_2) { create(:issue, :confidential, :closed, project: project, assignee: assignee, milestone: milestone) }
+ let!(:closed_security_issue_3) { create(:issue, :confidential, :closed, project: project, author: author, milestone: milestone) }
+ let!(:closed_security_issue_4) { create(:issue, :confidential, :closed, project: project, assignee: assignee, milestone: milestone) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) }
+
+ before do
+ project.team << [member, :developer]
+ end
+
+ describe '#closed_items_count' do
+ it 'should not count confidential issues for non project members' do
+ expect(milestone.closed_items_count(non_member)).to eq 2
+ end
+
+ it 'should count confidential issues for author' do
+ expect(milestone.closed_items_count(author)).to eq 4
+ end
+
+ it 'should count confidential issues for assignee' do
+ expect(milestone.closed_items_count(assignee)).to eq 4
+ end
+
+ it 'should count confidential issues for project members' do
+ expect(milestone.closed_items_count(member)).to eq 6
+ end
+
+ it 'should count all issues for admin' do
+ expect(milestone.closed_items_count(admin)).to eq 6
+ end
+ end
+
+ describe '#total_items_count' do
+ it 'should not count confidential issues for non project members' do
+ expect(milestone.total_items_count(non_member)).to eq 4
+ end
+
+ it 'should count confidential issues for author' do
+ expect(milestone.total_items_count(author)).to eq 7
+ end
+
+ it 'should count confidential issues for assignee' do
+ expect(milestone.total_items_count(assignee)).to eq 7
+ end
+
+ it 'should count confidential issues for project members' do
+ expect(milestone.total_items_count(member)).to eq 10
+ end
+
+ it 'should count all issues for admin' do
+ expect(milestone.total_items_count(admin)).to eq 10
+ end
+ end
+
+ describe '#complete?' do
+ it 'returns false when has items opened' do
+ expect(milestone.complete?(non_member)).to eq false
+ end
+
+ it 'returns true when all items are closed' do
+ issue.close
+ merge_request.close
+
+ expect(milestone.complete?(non_member)).to eq true
+ end
+ end
+
+ describe '#percent_complete' do
+ it 'should not count confidential issues for non project members' do
+ expect(milestone.percent_complete(non_member)).to eq 50
+ end
+
+ it 'should count confidential issues for author' do
+ expect(milestone.percent_complete(author)).to eq 57
+ end
+
+ it 'should count confidential issues for assignee' do
+ expect(milestone.percent_complete(assignee)).to eq 57
+ end
+
+ it 'should count confidential issues for project members' do
+ expect(milestone.percent_complete(member)).to eq 60
+ end
+
+ it 'should count confidential issues for admin' do
+ expect(milestone.percent_complete(admin)).to eq 60
+ end
+ end
+end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index ec2a923f91b..5fe44246738 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -65,6 +65,42 @@ describe Event, models: true do
it { expect(@event.author).to eq(@user) }
end
+ describe '#proper?' do
+ context 'issue event' do
+ let(:project) { create(:empty_project, :public) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:author) { create(:author) }
+ let(:assignee) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:event) { Event.new(project: project, action: Event::CREATED, target: issue, author_id: author.id) }
+
+ before do
+ project.team << [member, :developer]
+ end
+
+ context 'for non confidential issues' do
+ let(:issue) { create(:issue, project: project, author: author, assignee: assignee) }
+
+ it { expect(event.proper?(non_member)).to eq true }
+ it { expect(event.proper?(author)).to eq true }
+ it { expect(event.proper?(assignee)).to eq true }
+ it { expect(event.proper?(member)).to eq true }
+ it { expect(event.proper?(admin)).to eq true }
+ end
+
+ context 'for confidential issues' do
+ let(:issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
+
+ it { expect(event.proper?(non_member)).to eq false }
+ it { expect(event.proper?(author)).to eq true }
+ it { expect(event.proper?(assignee)).to eq true }
+ it { expect(event.proper?(member)).to eq true }
+ it { expect(event.proper?(admin)).to eq true }
+ end
+ end
+ end
+
describe '.limit_recent' do
let!(:event1) { create(:closed_issue_event) }
let!(:event2) { create(:closed_issue_event) }
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 8bf68013fd2..f2f07e4ee17 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -86,6 +86,41 @@ 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) }
+
+ context 'with diffs' do
+ subject { create(:merge_request, :with_diffs) }
+ it 'returns the sha of the source branch last commit' do
+ expect(subject.source_sha).to eq(last_branch_commit.sha)
+ end
+ end
+
+ context 'without diffs' do
+ subject { create(:merge_request, :without_diffs) }
+ it 'returns the sha of the source branch last commit' do
+ expect(subject.source_sha).to eq(last_branch_commit.sha)
+ end
+ end
+
+ context 'when the merge request is being created' do
+ subject { build(:merge_request, source_branch: nil, compare_commits: []) }
+ it 'returns nil' do
+ expect(subject.source_sha).to be_nil
+ end
+ end
+ end
+
describe '#to_reference' do
it 'returns a String reference to the object' do
expect(subject.to_reference).to eq "!#{subject.iid}"
@@ -150,6 +185,7 @@ describe MergeRequest, models: true do
let(:commit2) { double('commit2', safe_message: "Fixes #{issue1.to_reference}") }
before do
+ subject.project.team << [subject.author, :developer]
allow(subject).to receive(:commits).and_return([commit0, commit1, commit2])
end
@@ -180,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
@@ -205,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
@@ -284,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/milestone_spec.rb b/spec/models/milestone_spec.rb
index de1757bf67a..72a4ea70228 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -32,6 +32,7 @@ describe Milestone, models: true do
let(:milestone) { create(:milestone) }
let(:issue) { create(:issue) }
+ let(:user) { create(:user) }
describe "unique milestone title per project" do
it "shouldn't accept the same title in a project twice" do
@@ -50,18 +51,17 @@ describe Milestone, models: true do
describe "#percent_complete" do
it "should not count open issues" do
milestone.issues << issue
- expect(milestone.percent_complete).to eq(0)
+ expect(milestone.percent_complete(user)).to eq(0)
end
it "should count closed issues" do
issue.close
milestone.issues << issue
- expect(milestone.percent_complete).to eq(100)
+ expect(milestone.percent_complete(user)).to eq(100)
end
it "should recover from dividing by zero" do
- expect(milestone.issues).to receive(:size).and_return(0)
- expect(milestone.percent_complete).to eq(0)
+ expect(milestone.percent_complete(user)).to eq(0)
end
end
@@ -103,7 +103,7 @@ describe Milestone, models: true do
)
end
- it { expect(milestone.percent_complete).to eq(75) }
+ it { expect(milestone.percent_complete(user)).to eq(75) }
end
describe :items_count do
@@ -113,23 +113,23 @@ describe Milestone, models: true do
milestone.merge_requests << create(:merge_request)
end
- it { expect(milestone.closed_items_count).to eq(1) }
- it { expect(milestone.total_items_count).to eq(3) }
- it { expect(milestone.is_empty?).to be_falsey }
+ it { expect(milestone.closed_items_count(user)).to eq(1) }
+ it { expect(milestone.total_items_count(user)).to eq(3) }
+ it { expect(milestone.is_empty?(user)).to be_falsey }
end
describe :can_be_closed? do
it { expect(milestone.can_be_closed?).to be_truthy }
end
- describe :is_empty? do
+ describe :total_items_count do
before do
create :closed_issue, milestone: milestone
create :merge_request, milestone: milestone
end
it 'Should return total count of issues and merge requests assigned to milestone' do
- expect(milestone.total_items_count).to eq 2
+ expect(milestone.total_items_count(user)).to eq 2
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 536fe66b21b..a57229a4fdf 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -780,4 +780,34 @@ describe Repository, models: true do
end
end
end
+
+ describe '#build_cache' do
+ let(:cache) { repository.send(:cache) }
+
+ it 'builds the caches if they do not already exist' do
+ expect(cache).to receive(:exist?).
+ exactly(repository.cache_keys.length).
+ times.
+ and_return(false)
+
+ repository.cache_keys.each do |key|
+ expect(repository).to receive(key)
+ end
+
+ repository.build_cache
+ end
+
+ it 'does not build any caches that already exist' do
+ expect(cache).to receive(:exist?).
+ exactly(repository.cache_keys.length).
+ times.
+ and_return(true)
+
+ repository.cache_keys.each do |key|
+ expect(repository).to_not receive(key)
+ end
+
+ repository.build_cache
+ end
+ end
end
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/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 571ea2dae4c..bb2ab058003 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -3,7 +3,11 @@ require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
- let!(:project) { create(:project, namespace: user.namespace ) }
+ let(:non_member) { create(:user) }
+ let(:author) { create(:author) }
+ let(:assignee) { create(:assignee) }
+ let(:admin) { create(:admin) }
+ let!(:project) { create(:project, :public, namespace: user.namespace ) }
let!(:closed_issue) do
create :closed_issue,
author: user,
@@ -12,6 +16,13 @@ describe API::API, api: true do
state: :closed,
milestone: milestone
end
+ let!(:confidential_issue) do
+ create :issue,
+ :confidential,
+ project: project,
+ author: author,
+ assignee: assignee
+ end
let!(:issue) do
create :issue,
author: user,
@@ -123,10 +134,43 @@ describe API::API, api: true do
let(:base_url) { "/projects/#{project.id}" }
let(:title) { milestone.title }
- it "should return project issues" do
+ it 'should return project issues without confidential issues for non project members' do
+ get api("#{base_url}/issues", non_member)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'should return project confidential issues for author' do
+ get api("#{base_url}/issues", author)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'should return project confidential issues for assignee' do
+ get api("#{base_url}/issues", assignee)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'should return project issues with confidential issues for project members' do
get api("#{base_url}/issues", user)
expect(response.status).to eq(200)
expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response.first['title']).to eq(issue.title)
+ end
+
+ it 'should return project confidential issues for admin' do
+ get api("#{base_url}/issues", admin)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
expect(json_response.first['title']).to eq(issue.title)
end
@@ -206,6 +250,41 @@ describe API::API, api: true do
get api("/projects/#{project.id}/issues/54321", user)
expect(response.status).to eq(404)
end
+
+ context 'confidential issues' do
+ it "should return 404 for non project members" do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member)
+ expect(response.status).to eq(404)
+ end
+
+ it "should return confidential issue for project members" do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.id}", user)
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+
+ it "should return confidential issue for author" do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.id}", author)
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+
+ it "should return confidential issue for assignee" do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee)
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+
+ it "should return confidential issue for admin" do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin)
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+ end
end
describe "POST /projects/:id/issues" do
@@ -294,6 +373,35 @@ describe API::API, api: true do
expect(response.status).to eq(400)
expect(json_response['message']['labels']['?']['title']).to eq(['is invalid'])
end
+
+ context 'confidential issues' do
+ it "should return 403 for non project members" do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member),
+ title: 'updated title'
+ expect(response.status).to eq(403)
+ end
+
+ it "should update a confidential issue for project members" do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
+ title: 'updated title'
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it "should update a confidential issue for author" do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.id}", author),
+ title: 'updated title'
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it "should update a confidential issue for admin" do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin),
+ title: 'updated title'
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+ end
end
describe 'PUT /projects/:id/issues/:issue_id to update labels' do
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index b49ca96e8e8..8490a729e51 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -215,12 +215,16 @@ describe GitPushService, services: true do
let(:commit) { project.commit }
before do
+ project.team << [commit_author, :developer]
+ project.team << [user, :developer]
+
allow(commit).to receive_messages(
safe_message: "this commit \n mentions #{issue.to_reference}",
references: [issue],
author_name: commit_author.name,
author_email: commit_author.email
)
+
allow(project.repository).to receive(:commits_between).and_return([commit])
end
diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb
new file mode 100644
index 00000000000..6108c26a78b
--- /dev/null
+++ b/spec/services/projects/autocomplete_service_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe Projects::AutocompleteService, services: true do
+ describe '#issues' do
+ describe 'confidential issues' do
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:member) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:project) { create(:empty_project, :public) }
+ let!(:issue) { create(:issue, project: project, title: 'Issue 1') }
+ let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) }
+ let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) }
+
+ it 'should not list project confidential issues for guests' do
+ autocomplete = described_class.new(project, nil)
+ issues = autocomplete.issues.map(&:iid)
+
+ expect(issues).to include issue.iid
+ expect(issues).not_to include security_issue_1.iid
+ expect(issues).not_to include security_issue_2.iid
+ expect(issues.count).to eq 1
+ end
+
+ it 'should not list project confidential issues for non project members' do
+ autocomplete = described_class.new(project, non_member)
+ issues = autocomplete.issues.map(&:iid)
+
+ expect(issues).to include issue.iid
+ expect(issues).not_to include security_issue_1.iid
+ expect(issues).not_to include security_issue_2.iid
+ expect(issues.count).to eq 1
+ end
+
+ it 'should list project confidential issues for author' do
+ autocomplete = described_class.new(project, author)
+ issues = autocomplete.issues.map(&:iid)
+
+ expect(issues).to include issue.iid
+ expect(issues).to include security_issue_1.iid
+ expect(issues).not_to include security_issue_2.iid
+ expect(issues.count).to eq 2
+ end
+
+ it 'should list project confidential issues for assignee' do
+ autocomplete = described_class.new(project, assignee)
+ issues = autocomplete.issues.map(&:iid)
+
+ expect(issues).to include issue.iid
+ expect(issues).not_to include security_issue_1.iid
+ expect(issues).to include security_issue_2.iid
+ expect(issues.count).to eq 2
+ end
+
+ it 'should list project confidential issues for project members' do
+ project.team << [member, :developer]
+
+ autocomplete = described_class.new(project, member)
+ issues = autocomplete.issues.map(&:iid)
+
+ expect(issues).to include issue.iid
+ expect(issues).to include security_issue_1.iid
+ expect(issues).to include security_issue_2.iid
+ expect(issues.count).to eq 3
+ end
+
+ it 'should list all project issues for admin' do
+ autocomplete = described_class.new(project, admin)
+ issues = autocomplete.issues.map(&:iid)
+
+ expect(issues).to include issue.iid
+ expect(issues).to include security_issue_1.iid
+ expect(issues).to include security_issue_2.iid
+ expect(issues.count).to eq 3
+ end
+ 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
diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb
index fce91015fd4..e876d44c166 100644
--- a/spec/support/mentionable_shared_examples.rb
+++ b/spec/support/mentionable_shared_examples.rb
@@ -52,6 +52,8 @@ shared_context 'mentionable context' do
end
set_mentionable_text.call(ref_string)
+
+ project.team << [author, :developer]
end
end