diff options
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) @@ -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 -   - %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 +   + %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} · - elsif show_full_project_name %strong #{project.name_with_namespace} · + - 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 · = 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 |