diff options
author | Grzegorz Bizon <grzesiek.bizon@gmail.com> | 2016-03-19 18:50:15 +0100 |
---|---|---|
committer | Grzegorz Bizon <grzesiek.bizon@gmail.com> | 2016-03-19 18:50:15 +0100 |
commit | 0115ad66d264f4670f241251acd3e36991134576 (patch) | |
tree | e60e7773e909de64ccdeebd1cc97928d24073cd2 /app | |
parent | 9b13ce0b7a50e65dfba31d4865a728c725daa3fe (diff) | |
parent | 4f0302f00ef0c51b67b73429ace0a632971b7f1b (diff) | |
download | gitlab-ce-0115ad66d264f4670f241251acd3e36991134576.tar.gz |
Merge branch 'master' into feature/issue-move
* master: (121 commits)
Dedupe labels in labels selector in Dashboard pages
Refactor colors and lists
Add a safeguard in MergeRequest#compute_diverged_commits_count
Fix an issue when the target branch of a MR had been deleted
Add avatar to issue and MR pages header
Cleanup somce css colors
Re-group scss variables
Refactor `Todo#target`
Fixes issue with filter label missing on labels & milestones
Rename `Todo#to_reference` to `Todo#target_reference`
Fixed failing tests
Updated controller with before_action Fixed other issues based on feedback
Fixes issue on dashboard issues
Full labels data in JSON
Fixed issue with labels dropdown getting wrong labels
Update CHANGELOG
Use `Note#for_project_snippet?` to skip notes on project snippet
Use `Commit#short_id` instead of `Commit.truncate_sha`
Reuse `for_commit?` on conditional validations
Update schema info comment on todo related files
...
Conflicts:
app/models/issue.rb
db/schema.rb
spec/models/issue_spec.rb
Diffstat (limited to 'app')
109 files changed, 1199 insertions, 710 deletions
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..c81e8bf760a 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -246,11 +246,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/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 75d7f52bbb6..82532216589 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -343,6 +343,7 @@ class @Notes updateNote: (_xhr, note, _status) => # Convert returned HTML to a jQuery object so we can modify it further $html = $(note.html) + $('.js-timeago', $html).timeago() $html.syntaxHighlight() $html.find('.js-task-list-container').taskList('enable') @@ -626,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/project_new.js.coffee b/app/assets/javascripts/project_new.js.coffee index fecdb9fc2e7..63dee4ed5d7 100644 --- a/app/assets/javascripts/project_new.js.coffee +++ b/app/assets/javascripts/project_new.js.coffee @@ -3,3 +3,16 @@ class @ProjectNew $('.project-edit-container').on 'ajax:before', => $('.project-edit-container').hide() $('.save-project-loader').show() + @toggleSettings() + @toggleSettingsOnclick() + + + toggleSettings: -> + checked = $("#project_builds_enabled").prop("checked") + if checked + $('.builds-feature').show() + else + $('.builds-feature').hide() + + toggleSettingsOnclick: -> + $("#project_builds_enabled").on 'click', @toggleSettings 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/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 90c3ce0e84c..c36f29dda0e 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -28,10 +28,6 @@ border-bottom: 1px solid $border-color; color: $gl-gray; - a { - color: $md-link-color; - } - &.oneline-block { line-height: 42px; } 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/common.scss b/app/assets/stylesheets/framework/common.scss index 180926b3b97..bc03c2180be 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -8,20 +8,20 @@ /** COMMON CLASSES **/ .prepend-top-0 { margin-top: 0; } .prepend-top-5 { margin-top: 5px; } -.prepend-top-10 { margin-top:10px } +.prepend-top-10 { margin-top: 10px } .prepend-top-default { margin-top: $gl-padding !important; } -.prepend-top-20 { margin-top:20px } -.prepend-left-10 { margin-left:10px } +.prepend-top-20 { margin-top: 20px } +.prepend-left-10 { margin-left: 10px } .prepend-left-default { margin-left: $gl-padding; } -.prepend-left-20 { margin-left:20px } +.prepend-left-20 { margin-left: 20px } .append-right-5 { margin-right: 5px } -.append-right-10 { margin-right:10px } +.append-right-10 { margin-right: 10px } .append-right-default { margin-right: $gl-padding; } -.append-right-20 { margin-right:20px } -.append-bottom-0 { margin-bottom:0 } -.append-bottom-10 { margin-bottom:10px } -.append-bottom-15 { margin-bottom:15px } -.append-bottom-20 { margin-bottom:20px } +.append-right-20 { margin-right: 20px } +.append-bottom-0 { margin-bottom: 0 } +.append-bottom-10 { margin-bottom: 10px } +.append-bottom-15 { margin-bottom: 15px } +.append-bottom-20 { margin-bottom: 20px } .append-bottom-default { margin-bottom: $gl-padding; } .inline { display: inline-block } .center { text-align: center } @@ -134,10 +134,10 @@ p.time { // Fix issue with notes & lists creating a bunch of bottom borders. li.note { - img { max-width:100% } + img { max-width: 100% } .note-title { li { - border-bottom:none !important; + border-bottom: none !important; } } } 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 5e3546bc6ff..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; +$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/appearances.scss b/app/assets/stylesheets/pages/appearances.scss index e2070f17c3b..878f44116ba 100644 --- a/app/assets/stylesheets/pages/appearances.scss +++ b/app/assets/stylesheets/pages/appearances.scss @@ -4,7 +4,7 @@ } .appearance-light-logo-preview { - background-color: $background-color; + background-color: $background-color; max-width: 72px; padding: 10px; margin-bottom: 10px; 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/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss index 88639399148..cf7567513ec 100644 --- a/app/assets/stylesheets/pages/dashboard.scss +++ b/app/assets/stylesheets/pages/dashboard.scss @@ -11,15 +11,15 @@ } .dashboard-search-filter { - padding:5px; + padding: 5px; .search-text-input { - float:left; + float: left; @extend .col-md-2; } .btn { margin-left: 5px; - float:left; + float: left; } } 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 e7da0a2f689..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 { @@ -94,7 +91,7 @@ } } - &:last-child { border:none } + &:last-child { border: none } .event_commits { li { @@ -138,7 +135,7 @@ @include str-truncated(100%); padding: 5px 0; font-size: 13px; - float:left; + float: left; margin-right: -150px; padding-right: 150px; line-height: 20px; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index faa2ebfda78..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,20 +23,25 @@ padding: 6px 10px; } } + + &.has-labels { + margin-bottom: -5px; + } } .issuable-sidebar { .block { @include clearfix; - padding: $gl-padding 0; + padding: $gl-padding 0; border-bottom: 1px solid $border-gray-light; // This prevents the mess when resizing the sidebar // of elements repositioning themselves.. 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 { @@ -262,3 +263,37 @@ color: $gray-darkest; } } + +.edited-text { + color: $gray-darkest; + + .author_link { + 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 73718ff511a..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; } @@ -49,7 +49,7 @@ form.edit-issue { margin: 0; } -.merge-requests-title { +.merge-requests-title, .related-branches-title { font-size: 16px; font-weight: 600; } @@ -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/login.scss b/app/assets/stylesheets/pages/login.scss index d9c47881265..bc41f7d306f 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -28,7 +28,7 @@ img { max-width: 100%; - margin-bottom: 30px; + margin-bottom: 30px; } a { @@ -85,7 +85,7 @@ &.middle { border-top: 0; - margin-bottom:0; + margin-bottom: 0; @include border-radius(0); } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 969c79a9be9..d408853cc80 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -3,9 +3,9 @@ */ @-webkit-keyframes targe3-note { - from { background:#fffff0; } - 50% { background:#ffffd3; } - to { background:#fffff0; } + from { background: #fffff0; } + 50% { background: #ffffd3; } + to { background: #fffff0; } } ul.notes { @@ -93,12 +93,12 @@ ul.notes { .discussion { overflow: hidden; display: block; - position:relative; + position: relative; } .note { display: block; - position:relative; + position: relative; .note-body { overflow: auto; @@ -108,6 +108,13 @@ ul.notes { word-wrap: break-word; @include md-typography; + // On diffs code should wrap nicely and not overflow + pre { + code { + white-space: pre-wrap; + } + } + // Reset ul style types since we're nested inside a ul already & > ul { list-style-type: disc; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 3fe2c9a3346..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; + } } } @@ -286,11 +293,11 @@ table.table.protected-branches-list tr.no-border { padding-bottom: 4px; ul.nav { - display:inline-block; + display: inline-block; } .nav li { - display:inline; + display: inline; } .nav > li > a { @@ -303,11 +310,11 @@ table.table.protected-branches-list tr.no-border { } li { - display:inline; + display: inline; } a { - float:left; + float: left; font-size: 17px; } 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 ef63b010600..25b5e95583e 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -41,12 +41,12 @@ vertical-align: middle; i, a { - color: $gl-link-color; + color: $gl-dark-link-color; } img { position: relative; - top:-1px; + top: -1px; } } diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 3063d299b1a..9abf08d0e19 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -150,7 +150,7 @@ class Admin::UsersController < Admin::ApplicationController :email, :remember_me, :bio, :name, :username, :skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :force_random_password, :extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, :hide_no_password, - :projects_limit, :can_create_group, :admin, :key_id + :projects_limit, :can_create_group, :admin, :key_id, :external ) end 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/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb index dc9c96df003..6ff47c4033a 100644 --- a/app/controllers/projects/badges_controller.rb +++ b/app/controllers/projects/badges_controller.rb @@ -1,5 +1,5 @@ class Projects::BadgesController < Projects::ApplicationController - before_action :set_no_cache + before_action :no_cache_headers def build respond_to do |format| @@ -10,15 +10,4 @@ class Projects::BadgesController < Projects::ApplicationController end end end - - private - - def set_no_cache - expires_now - - # Add some deprecated headers for older agents - # - response.headers['Pragma'] = 'no-cache' - response.headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT' - end end diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 4db3b3bf23d..43ea717cbd2 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -9,7 +9,7 @@ class Projects::BranchesController < Projects::ApplicationController @sort = params[:sort] || 'name' @branches = @repository.branches_sorted_by(@sort) @branches = Kaminari.paginate_array(@branches).page(params[:page]).per(PER_PAGE) - + @max_commits = @branches.reduce(0) do |memo, branch| diverging_commit_counts = repository.diverging_commit_counts(branch) [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max @@ -23,11 +23,15 @@ class Projects::BranchesController < Projects::ApplicationController def create branch_name = sanitize(strip_tags(params[:branch_name])) branch_name = Addressable::URI.unescape(branch_name) - ref = sanitize(strip_tags(params[:ref])) - ref = Addressable::URI.unescape(ref) + result = CreateBranchService.new(project, current_user). execute(branch_name, ref) + if params[:issue_iid] + issue = @project.issues.find_by(iid: params[:issue_iid]) + SystemNoteService.new_issue_branch(issue, @project, current_user, branch_name) if issue + end + if result[:status] == :success @branch = result[:branch] redirect_to namespace_project_tree_path(@project.namespace, @project, @@ -49,4 +53,15 @@ class Projects::BranchesController < Projects::ApplicationController format.js { render status: status[:return_code] } end end + + private + + def ref + if params[:ref] + ref_escaped = sanitize(strip_tags(params[:ref])) + Addressable::URI.unescape(ref_escaped) + else + @project.default_branch + end + end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 016aa55656e..aa73c6df545 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] @@ -65,6 +65,7 @@ class Projects::IssuesController < Projects::ApplicationController @notes = @issue.notes.nonawards.with_associations.fresh @noteable = @issue @merge_requests = @issue.referenced_merge_requests(current_user) + @related_branches = @issue.related_branches - @merge_requests.map(&:source_branch) respond_with(@issue) end @@ -133,6 +134,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 @@ -163,7 +168,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/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/finders/projects_finder.rb b/app/finders/projects_finder.rb index 2c55f088594..3a5fc5b5907 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -40,25 +40,26 @@ class ProjectsFinder private def group_projects(current_user, group) - if current_user - [ - group_projects_for_user(current_user, group), - group.projects.public_and_internal_only, - group.shared_projects.visible_to_user(current_user) - ] + return [group.projects.public_only] unless current_user + + user_group_projects = [ + group_projects_for_user(current_user, group), + group.shared_projects.visible_to_user(current_user) + ] + if current_user.external? + user_group_projects << group.projects.public_only else - [group.projects.public_only] + user_group_projects << group.projects.public_and_internal_only end end def all_projects(current_user) - if current_user - [ - current_user.authorized_projects, - public_and_internal_projects - ] + return [public_projects] unless current_user + + if current_user.external? + [current_user.authorized_projects, public_projects] else - [Project.public_only] + [current_user.authorized_projects, public_and_internal_projects] end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index d1b1c61b710..e6ceb213532 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -182,7 +182,7 @@ module ApplicationHelper # Returns an HTML-safe String def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false) element = content_tag :time, time.to_s, - class: "#{html_class} js-timeago js-timeago-pending", + class: "#{html_class} js-timeago #{"js-timeago-pending" unless skip_js}", datetime: time.to_time.getutc.iso8601, title: time.in_time_zone.to_s(:medium), data: { toggle: 'tooltip', placement: placement, container: 'body' } @@ -196,6 +196,22 @@ module ApplicationHelper element end + def edited_time_ago_with_tooltip(object, placement: 'top', html_class: 'time_ago', include_author: false) + return if object.updated_at == object.created_at + + content_tag :small, class: "edited-text" do + output = content_tag(:span, "Edited ") + output << time_ago_with_tooltip(object.updated_at, placement: placement, html_class: html_class) + + if include_author && object.updated_by && object.updated_by != object.author + output << content_tag(:span, " by ") + output << link_to_member(object.project, object.updated_by, avatar: false, author_class: nil) + end + + output + end + end + def render_markup(file_name, file_content) if gitlab_markdown?(file_name) Haml::Helpers.preserve(markdown(file_content)) @@ -285,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/issuables_helper.rb b/app/helpers/issuables_helper.rb index 2dfeddf7368..81df2094392 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -20,6 +20,23 @@ module IssuablesHelper base_issuable_scope(issuable).where('iid < ?', issuable.iid).first end + def user_dropdown_label(user_id, default_label) + return "Unassigned" if user_id == "0" + + if @project + member = @project.team.find_member(user_id) + user = member.user if member + else + user = User.find_by(id: user_id) + end + + if user + user.name + else + default_label + end + end + private def sidebar_gutter_collapsed? diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index b67057ebc7c..24b90fef4fe 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -111,6 +111,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..e238a7b4c26 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -109,19 +109,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 fe9e0aab717..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 [] @@ -109,23 +111,10 @@ class Ability key = "/user/#{user.id}/project/#{project.id}" RequestStore.store[key] ||= begin - team = project.team - - # Rules based on role in project - if team.master?(user) - rules.push(*project_master_rules) - - elsif team.developer?(user) - rules.push(*project_dev_rules) - - elsif team.reporter?(user) - rules.push(*project_report_rules) + # Push abilities on the users team role + rules.push(*project_team_rules(project.team, user)) - elsif team.guest?(user) - rules.push(*project_guest_rules) - end - - if project.public? || project.internal? + if project.public? || (project.internal? && !user.external?) rules.push(*public_project_rules) # Allow to read builds for internal projects @@ -148,6 +137,19 @@ class Ability end end + def project_team_rules(team, user) + # Rules based on role in project + if team.master?(user) + project_master_rules + elsif team.developer?(user) + project_dev_rules + elsif team.reporter?(user) + project_report_rules + elsif team.guest?(user) + project_guest_rules + end + end + def public_project_rules @public_project_rules ||= project_guest_rules + [ :download_code, @@ -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 @@ -356,7 +359,7 @@ class Ability ] end - if snippet.public? || snippet.internal? + if snippet.public? || (snippet.internal? && !user.external?) rules << :read_personal_snippet 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/commit_status.rb b/app/models/commit_status.rb index 3b1aa0f5c80..3377a85a55a 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -114,7 +114,7 @@ class CommitStatus < ActiveRecord::Base end def ignored? - failed? && allow_failure? + allow_failure? && (failed? || canceled?) end def duration 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 6a016636e0d..6a9253c3385 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -61,6 +61,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 @@ -90,11 +97,21 @@ class Issue < ActiveRecord::Base end def referenced_merge_requests(current_user = nil) - Gitlab::ReferenceExtractor.lazily do - [self, *notes].flat_map do |note| - note.all_references(current_user).merge_requests - end - end.sort_by(&:iid) + @referenced_merge_requests ||= {} + @referenced_merge_requests[current_user] ||= begin + Gitlab::ReferenceExtractor.lazily do + [self, *notes].flat_map do |note| + note.all_references(current_user).merge_requests + end + end.sort_by(&:iid).uniq + end + end + + def related_branches + return [] if self.project.empty_repo? + self.project.repository.branch_names.select do |branch| + branch =~ /\A#{iid}-(?!\d+-stable)/i + end end # Reset issue events cache @@ -135,4 +152,15 @@ class Issue < ActiveRecord::Base !moved? && user.can?(:admin_issue, self.project) end + + def to_branch_name + "#{iid}-#{title.parameterize}" + end + + def can_be_worked_on?(current_user) + !self.closed? && + !self.project.forked? && + self.related_branches.empty? && + self.closed_by_merge_requests(current_user).empty? + end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 188325045e2..a6140b5b04c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -516,11 +516,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 +572,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/project.rb b/app/models/project.rb index 89a55a510cd..412c6c6732d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -254,12 +254,6 @@ class Project < ActiveRecord::Base where('projects.last_activity_at < ?', 6.months.ago) end - def publicish(user) - visibility_levels = [Project::PUBLIC] - visibility_levels << Project::INTERNAL if user - where(visibility_level: visibility_levels) - end - def with_push joins(:events).where('events.action = ?', Event::PUSHED) end @@ -577,10 +571,7 @@ class Project < ActiveRecord::Base end def avatar_in_git - @avatar_file ||= 'logo.png' if repository.blob_at_branch('master', 'logo.png') - @avatar_file ||= 'logo.jpg' if repository.blob_at_branch('master', 'logo.jpg') - @avatar_file ||= 'logo.gif' if repository.blob_at_branch('master', 'logo.gif') - @avatar_file + repository.avatar end def avatar_url diff --git a/app/models/repository.rb b/app/models/repository.rb index e555e97689d..25d24493f6e 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -3,6 +3,10 @@ require 'securerandom' class Repository class CommitError < StandardError; end + # Files to use as a project avatar in case no avatar was uploaded via the web + # UI. + AVATAR_FILES = %w{logo.png logo.jpg logo.gif} + include Gitlab::ShellAdapter attr_accessor :path_with_namespace, :project @@ -223,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 @@ -241,12 +239,13 @@ class Repository @branches = nil end - def expire_cache(branch_name = nil) + def expire_cache(branch_name = nil, revision = nil) cache_keys.each do |key| cache.expire(key) end expire_branch_cache(branch_name) + expire_avatar_cache(branch_name, revision) # This ensures this particular cache is flushed after the first commit to a # new repository. @@ -296,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 @@ -316,6 +303,23 @@ class Repository cache.expire(:branch_names) end + def expire_avatar_cache(branch_name = nil, revision = nil) + # Avatars are pulled from the default branch, thus if somebody pushes to a + # different branch there's no need to expire anything. + return if branch_name && branch_name != root_ref + + # We don't want to flush the cache if the commit didn't actually make any + # changes to any of the possible avatar files. + if revision && commit = self.commit(revision) + return unless commit.diffs. + any? { |diff| AVATAR_FILES.include?(diff.new_path) } + end + + cache.expire(:avatar) + + @avatar = nil + end + # Runs code just before a repository is deleted. def before_delete expire_cache if exists? @@ -350,8 +354,8 @@ class Repository end # Runs code after a new commit has been pushed. - def after_push_commit(branch_name) - expire_cache(branch_name) + def after_push_commit(branch_name, revision) + expire_cache(branch_name, revision) end # Runs code after a new branch has been created. @@ -857,6 +861,14 @@ class Repository end end + def avatar + @avatar ||= cache.fetch(:avatar) do + AVATAR_FILES.find do |file| + blob_at_branch('master', file) + end + end + end + private def cache 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/models/user.rb b/app/models/user.rb index 5ba72dc0ae9..9c315cfe966 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -59,6 +59,7 @@ # hide_project_limit :boolean default(FALSE) # unlock_token :string # otp_grace_period_started_at :datetime +# external :boolean default(FALSE) # require 'carrierwave/orm/activerecord' @@ -77,6 +78,7 @@ class User < ActiveRecord::Base add_authentication_token_field :authentication_token default_value_for :admin, false + default_value_for :external, false default_value_for :can_create_group, gitlab_config.default_can_create_group default_value_for :can_create_team, false default_value_for :hide_no_ssh_key, false @@ -171,6 +173,7 @@ class User < ActiveRecord::Base after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? } before_save :ensure_authentication_token + before_save :ensure_external_user_rights after_save :ensure_namespace_correct after_initialize :set_projects_limit after_create :post_create_hook @@ -218,6 +221,7 @@ class User < ActiveRecord::Base # Scopes scope :admins, -> { where(admin: true) } scope :blocked, -> { with_states(:blocked, :ldap_blocked) } + scope :external, -> { where(external: true) } scope :active, -> { with_state(:active) } scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all } scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') } @@ -273,6 +277,8 @@ class User < ActiveRecord::Base self.with_two_factor when 'wop' self.without_projects + when 'external' + self.external else self.active end @@ -841,4 +847,11 @@ class User < ActiveRecord::Base def send_devise_notification(notification, *args) devise_mailer.send(notification, self, *args).deliver_later end + + def ensure_external_user_rights + return unless self.external? + + self.can_create_group = false + self.projects_limit = 0 + 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/git_push_service.rb b/app/services/git_push_service.rb index 855d55b3e58..c007d648dd6 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -17,7 +17,7 @@ class GitPushService < BaseService # 6. Checks if the project's main language has changed # def execute - @project.repository.after_push_commit(branch_name) + @project.repository.after_push_commit(branch_name, params[:newrev]) if push_remove_branch? @project.repository.after_remove_branch diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 954746a39a5..fa34753c4fd 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -47,6 +47,21 @@ module MergeRequests merge_request.title = merge_request.source_branch.titleize.humanize end + # When your branch name starts with an iid followed by a dash this pattern will + # 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+)-/) + iid = match[1] + closes_issue = "Closes ##{iid}" + + if merge_request.description.present? + merge_request.description << closes_issue.prepend("\n") + else + merge_request.description = closes_issue + end + end + merge_request end 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 c679891b07c..b2b5c8b83bd 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -207,6 +207,18 @@ class SystemNoteService create_note(noteable: noteable, project: project, author: author, note: body) end + # Called when a branch is created from the 'new branch' button on a issue + # Example note text: + # + # "Started branch `201-issue-branch-button`" + 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) + + body = "Started branch [`#{branch}`](#{link})" + create_note(noteable: issue, project: project, author: author, note: body) + end + # Called when a Mentionable references a Noteable # # noteable - Noteable object being referenced 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/users/_form.html.haml b/app/views/admin/users/_form.html.haml index e18dd9bc905..d2527ede995 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -58,9 +58,15 @@ = f.label :admin, class: 'control-label' - if current_user == @user .col-sm-10= f.check_box :admin, disabled: true - .col-sm-10 You cannot remove your own admin rights + .col-sm-10 You cannot remove your own admin rights. - else .col-sm-10= f.check_box :admin + + .form-group + = f.label :external, class: 'control-label' + .col-sm-10= f.check_box :external + .col-sm-10 External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups. + %fieldset %legend Profile .form-group diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index b6b1168bd37..0ee8dc962b9 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -19,6 +19,10 @@ = link_to admin_users_path(filter: 'two_factor_disabled') do 2FA Disabled %small.badge= number_with_delimiter(User.without_two_factor.count) + %li.filter-external{class: "#{'active' if params[:filter] == 'external'}"} + = link_to admin_users_path(filter: 'external') do + External + %small.badge= number_with_delimiter(User.external.count) %li{class: "#{'active' if params[:filter] == "blocked"}"} = link_to admin_users_path(filter: "blocked") do Blocked @@ -70,12 +74,14 @@ %li .list-item-name - if user.blocked? - %i.fa.fa-lock.cred + = icon("lock", class: "cred") - else - %i.fa.fa-user.cgreen + = icon("user", class: "cgreen") = link_to user.name, [:admin, user] - if user.admin? %strong.cred (Admin) + - if user.external? + %strong.cred (External) - if user == current_user %span.cred It's you! .pull-right diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 2bdbae19588..d37489bebea 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -48,6 +48,10 @@ Disabled %li + %span.light External User: + %strong + = @user.external? ? "Yes" : "No" + %li %span.light Can create groups: %strong = @user.can_create_group ? "Yes" : "No" diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml index c3efa7727b1..d54c7cad7be 100644 --- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml +++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml @@ -1,4 +1,4 @@ -- publicish_project_count = Project.publicish(current_user).count +- publicish_project_count = ProjectsFinder.new.execute(current_user).count %h3.page-title Welcome to GitLab! %p.light Self hosted Git management application. %hr @@ -18,7 +18,7 @@ - if current_user.can_create_project? .link_holder = link_to new_project_path, class: "btn btn-new" do - %i.fa.fa-plus + = icon('plus') New Project - if current_user.can_create_group? 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/_builds_settings.html.haml b/app/views/projects/_builds_settings.html.haml new file mode 100644 index 00000000000..95ab9ecf3e8 --- /dev/null +++ b/app/views/projects/_builds_settings.html.haml @@ -0,0 +1,60 @@ +%fieldset.builds-feature + %legend + Builds: + .form-group + .col-sm-offset-2.col-sm-10 + %p Get recent application code using the following command: + .radio + = f.label :build_allow_git_fetch_false do + = f.radio_button :build_allow_git_fetch, 'false' + %strong git clone + %br + %span.descr Slower but makes sure you have a clean dir before every build + .radio + = f.label :build_allow_git_fetch_true do + = f.radio_button :build_allow_git_fetch, 'true' + %strong git fetch + %br + %span.descr Faster + + .form-group + = f.label :build_timeout_in_minutes, 'Timeout', class: 'control-label' + .col-sm-10 + = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0' + %p.help-block per build in minutes + .form-group + = f.label :build_coverage_regex, "Test coverage parsing", class: 'control-label' + .col-sm-10 + .input-group + %span.input-group-addon / + = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered' + %span.input-group-addon / + %p.help-block + We will use this regular expression to find test coverage output in build trace. + Leave blank if you want to disable this feature + .bs-callout.bs-callout-info + %p Below are examples of regex for existing tools: + %ul + %li + Simplecov (Ruby) - + %code \(\d+.\d+\%\) covered + %li + pytest-cov (Python) - + %code \d+\%\s*$ + %li + phpunit --coverage-text --colors=never (PHP) - + %code ^\s*Lines:\s*\d+.\d+\% + + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :public_builds do + = f.check_box :public_builds + %strong Public builds + .help-block Allow everyone to access builds for Public and Internal projects + + .form-group + = f.label :runners_token, "Runners token", class: 'control-label' + .col-sm-10 + = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89' + %p.help-block The secure token used to checkout project. diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 3ac058a3bf8..dc34032b1b8 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 + - else + .nothing-here-block No preview for this file type diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index f2e56081afe..6d872cd0b21 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -84,6 +84,8 @@ %br %span.descr Share code pastes with others out of git repository + = render 'builds_settings', f: f + %fieldset.features %legend Project avatar: @@ -110,69 +112,6 @@ %hr = link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" - %fieldset.features - %legend - Continuous Integration - .form-group - .col-sm-offset-2.col-sm-10 - %p Get recent application code using the following command: - .radio - = f.label :build_allow_git_fetch_false do - = f.radio_button :build_allow_git_fetch, 'false' - %strong git clone - %br - %span.descr Slower but makes sure you have a clean dir before every build - .radio - = f.label :build_allow_git_fetch_true do - = f.radio_button :build_allow_git_fetch, 'true' - %strong git fetch - %br - %span.descr Faster - - .form-group - = f.label :build_timeout_in_minutes, 'Timeout', class: 'control-label' - .col-sm-10 - = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0' - %p.help-block per build in minutes - .form-group - = f.label :build_coverage_regex, "Test coverage parsing", class: 'control-label' - .col-sm-10 - .input-group - %span.input-group-addon / - = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered' - %span.input-group-addon / - %p.help-block - We will use this regular expression to find test coverage output in build trace. - Leave blank if you want to disable this feature - .bs-callout.bs-callout-info - %p Below are examples of regex for existing tools: - %ul - %li - Simplecov (Ruby) - - %code \(\d+.\d+\%\) covered - %li - pytest-cov (Python) - - %code \d+\%\s*$ - %li - phpunit --coverage-text --colors=never (PHP) - - %code ^\s*Lines:\s*\d+.\d+\% - - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :public_builds do - = f.check_box :public_builds - %strong Public builds - .help-block Allow everyone to access builds for Public and Internal projects - - %fieldset.features - %legend - Advanced settings - .form-group - = f.label :runners_token, "CI token", class: 'control-label' - .col-sm-10 - = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89' - %p.help-block The secure token used to checkout project. .form-actions = f.submit 'Save changes', class: "btn btn-save" 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/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index d9868ad1f0a..d6b38b327ff 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -1,4 +1,4 @@ --if @merge_requests.any? +- if @merge_requests.any? %h2.merge-requests-title = pluralize(@merge_requests.count, 'Related Merge Request') %ul.unstyled-list diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml new file mode 100644 index 00000000000..e66e4669d48 --- /dev/null +++ b/app/views/projects/issues/_new_branch.html.haml @@ -0,0 +1,5 @@ +- if current_user && can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user) + .pull-right + = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), method: :post, class: 'btn', title: @issue.to_branch_name do + = icon('code-fork') + New Branch diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml new file mode 100644 index 00000000000..b10cd03515f --- /dev/null +++ b/app/views/projects/issues/_related_branches.html.haml @@ -0,0 +1,15 @@ +- if @related_branches.any? + %h2.related-branches-title + = pluralize(@related_branches.count, 'Related Branch') + %ul.unstyled-list + - @related_branches.each do |branch| + %li + - sha = @project.repository.find_branch(branch).target + - ci_commit = @project.ci_commit(sha) if sha + - if ci_commit + %span.related-branch-ci-status + = render_ci_status(ci_commit) + %span.related-branch-info + %strong + = link_to namespace_project_compare_path(@project.namespace, @project, from: @project.default_branch, to: branch), class: "label-branch" do + = branch diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 0242276cd84..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) @@ -63,15 +63,14 @@ = markdown(@issue.description, cache_key: [@issue, "description"]) %textarea.hidden.js-task-list-field = @issue.description - - if @issue.updated_at != @issue.created_at - %small - Edited - = time_ago_with_tooltip(@issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago') + = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago') .merge-requests = render 'merge_requests' + = render 'related_branches' .content-block.content-block-small + = render 'new_branch' = render 'votes/votes_block', votable: @issue .row 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_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml index 602f787e6cf..a23bd8d18d0 100644 --- a/app/views/projects/merge_requests/show/_mr_box.html.haml +++ b/app/views/projects/merge_requests/show/_mr_box.html.haml @@ -11,7 +11,4 @@ %textarea.hidden.js-task-list-field = @merge_request.description - - if @merge_request.updated_at != @merge_request.created_at - %small - Edited - = time_ago_with_tooltip(@merge_request.updated_at, placement: 'bottom') + = edited_time_ago_with_tooltip(@merge_request, placement: 'bottom') 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/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/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 52972576aff..2cf32e6093d 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -27,20 +27,13 @@ %span.note-last-update %a{name: dom_id(note), href: "##{dom_id(note)}", title: 'Link here'} = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note_created_ago') - - if note.updated_at != note.created_at - %span.note-updated-at - · - = icon('edit', title: 'edited') - = time_ago_with_tooltip(note.updated_at, placement: 'bottom', html_class: 'note_edited_ago') - - if note.updated_by && note.updated_by != note.author - by #{link_to_member(note.project, note.updated_by, avatar: false, author_class: nil)} - .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''} .note-text = preserve do = markdown(note.note, pipeline: :note, cache_key: [note, "note"]) - if note_editable?(note) = render 'projects/notes/edit_form', note: note + = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) - if note.attachment.url .note-attachment @@ -54,4 +47,3 @@ = link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note), title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do = icon('trash-o', class: 'cred') - .clear 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/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 3eb0db276b2..ac20f7d1f7e 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -9,75 +9,20 @@ .filter-item.inline - if params[:author_id] = hidden_field_tag(:author_id, params[:author_id]) - = dropdown_tag("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" } }) + = 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", default_label: "Author" } }) .filter-item.inline - if params[:assignee_id] = hidden_field_tag(:assignee_id, params[:assignee_id]) - = dropdown_tag("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" } }) + = 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", default_label: "Assignee" } }) .filter-item.inline.milestone-filter - - if params[:milestone_title] - = hidden_field_tag(:milestone_title, params[:milestone_title]) - = dropdown_tag("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 - 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 d2fa407f8d3..9be3ca1041e 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -29,6 +29,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/_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/app/workers/post_receive.rb b/app/workers/post_receive.rb index 14d7813412e..3cc232ef1ae 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -1,6 +1,5 @@ class PostReceive include Sidekiq::Worker - include Gitlab::Identifier sidekiq_options queue: :post_receive @@ -11,51 +10,44 @@ class PostReceive log("Check gitlab.yml config for correct gitlab_shell.repos_path variable. \"#{Gitlab.config.gitlab_shell.repos_path}\" does not match \"#{repo_path}\"") end - repo_path.gsub!(/\.git\z/, "") - repo_path.gsub!(/\A\//, "") + post_received = Gitlab::GitPostReceive.new(repo_path, identifier, changes) - project = Project.find_with_namespace(repo_path) - - if project.nil? + if post_received.project.nil? log("Triggered hook for non-existing project with full path \"#{repo_path} \"") return false end - changes = Base64.decode64(changes) unless changes.include?(" ") - changes = utf8_encode_changes(changes) - changes = changes.lines + if post_received.wiki? + # Nothing defined here yet. + elsif post_received.regular_project? + process_project_changes(post_received) + else + log("Triggered hook for unidentifiable repository type with full path \"#{repo_path} \"") + false + end + end - changes.each do |change| + def process_project_changes(post_received) + post_received.changes.each do |change| oldrev, newrev, ref = change.strip.split(' ') - @user ||= identify(identifier, project, newrev) + @user ||= post_received.identify(newrev) unless @user - log("Triggered hook for non-existing user \"#{identifier} \"") + log("Triggered hook for non-existing user \"#{post_received.identifier} \"") return false end if Gitlab::Git.tag_ref?(ref) - GitTagPushService.new.execute(project, @user, oldrev, newrev, ref) + GitTagPushService.new.execute(post_received.project, @user, oldrev, newrev, ref) else - GitPushService.new(project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute + GitPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute end end end - def utf8_encode_changes(changes) - changes = changes.dup - - changes.force_encoding("UTF-8") - return changes if changes.valid_encoding? - - # Convert non-UTF-8 branch/tag names to UTF-8 so they can be dumped as JSON. - detection = CharlockHolmes::EncodingDetector.detect(changes) - return changes unless detection && detection[:encoding] - - CharlockHolmes::Converter.convert(changes, detection[:encoding], 'UTF-8') - end - + private + def log(message) Gitlab::GitLogger.error("POST-RECEIVE: #{message}") end |