diff options
author | Douwe Maan <douwe@selenight.nl> | 2016-03-20 14:57:25 +0100 |
---|---|---|
committer | Douwe Maan <douwe@selenight.nl> | 2016-03-20 14:57:25 +0100 |
commit | 2eb19ea3ea36916bbea72a8ccab3e6d15f602ac9 (patch) | |
tree | 55c08bbb50e92ce76028f68a5267401a76bc4b02 | |
parent | 8b830b8c3b32774e8ccf562b8bc9dbce3ecf3073 (diff) | |
parent | 01fe50a72513a88f2168c8c0a649661b1382a42b (diff) | |
download | gitlab-ce-2eb19ea3ea36916bbea72a8ccab3e6d15f602ac9.tar.gz |
Merge branch 'master' into issue_12658
256 files changed, 3640 insertions, 1423 deletions
diff --git a/CHANGELOG b/CHANGELOG index 0725649306e..d3171da81c3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,19 +1,29 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.6.0 (unreleased) + - Fix bug where wrong commit ID was being used in a merge request diff to show old image (Stan Hu) + - Add confidential issues - Bump gitlab_git to 9.0.3 (Stan Hu) - Support Golang subpackage fetching (Stan Hu) - Bump Capybara gem to 2.6.2 (Stan Hu) + - New branch button appears on issues where applicable - Contributions to forked projects are included in calendar - Improve the formatting for the user page bio (Connor Shea) + - Easily (un)mark merge request as WIP using link + - Use specialized system notes when MR is (un)marked as WIP - Removed the default password from the initial admin account created during setup. A password can be provided during setup (see installation docs), or GitLab will ask the user to create a new one upon first visit. - Fix issue when pushing to projects ending in .wiki + - Add support for wiki with UTF-8 page names (Hiroyuki Sato) + - Fix wiki search results point to raw source (Hiroyuki Sato) - Don't load all of GitLab in mail_room + - HTTP error pages work independently from location and config (Artem Sidorenko) - Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set - Memoize @group in Admin::GroupsController (Yatish Mehta) - Indicate how much an MR diverged from the target branch (Pierre de La Morinerie) + - Added omniauth-auth0 Gem (Daniel Carraro) + - Add label description in tooltip to labels in issue index and sidebar - Strip leading and trailing spaces in URL validator (evuez) - Add "last_sign_in_at" and "confirmed_at" to GET /users/* API endpoints for admins (evuez) - Return empty array instead of 404 when commit has no statuses in commit status API @@ -21,6 +31,7 @@ v 8.6.0 (unreleased) - Rewrite logo to simplify SVG code (Sean Lang) - Allow to use YAML anchors when parsing the `.gitlab-ci.yml` (Pascal Bach) - Ignore jobs that start with `.` (hidden jobs) + - Hide builds from project's settings when the feature is disabled - Allow to pass name of created artifacts archive in `.gitlab-ci.yml` - Refactor and greatly improve search performance - Add support for cross-project label references @@ -32,14 +43,26 @@ v 8.6.0 (unreleased) - Fix bug where Bitbucket `closed` issues were imported as `opened` (Iuri de Silvio) - Don't show Issues/MRs from archived projects in Groups view - Fix wrong "iid of max iid" in Issuable sidebar for some merged MRs + - Fix empty source_sha on Merge Request when there is no diff (Pierre de La Morinerie) - Increase the notes polling timeout over time (Roberto Dip) - Add shortcut to toggle markdown preview (Florent Baldino) - Show labels in dashboard and group milestone views + - Fix an issue when the target branch of a MR had been deleted - Add main language of a project in the list of projects (Tiago Botelho) + - Add #upcoming filter to Milestone filter (Tiago Botelho) - Add ability to show archived projects on dashboard, explore and group pages - Move group activity to separate page - Create external users which are excluded of internal and private projects unless access was explicitly granted - Continue parameters are checked to ensure redirection goes to the same instance + - User deletion is now done in the background so the request can not time out + - Canceled builds are now ignored in compound build status if marked as `allowed to fail` + - Trigger a todo for mentions on commits page + +v 8.5.8 + - Bump Git version requirement to 2.7.4 + +v 8.5.7 + - Bump Git version requirement to 2.7.3 v 8.5.6 - Obtain a lease before querying LDAP @@ -49,7 +72,6 @@ v 8.5.5 - Prevent a 500 error in Todos when author was removed - Fix pagination for filtered dashboard and explore pages - Fix "Show all" link behavior - - Add #upcoming filter to Milestone filter (Tiago Botelho) v 8.5.4 - Do not cache requests for badges (including builds badge) @@ -22,6 +22,7 @@ gem 'devise', '~> 3.5.4' gem 'devise-async', '~> 0.9.0' gem 'doorkeeper', '~> 2.2.0' gem 'omniauth', '~> 1.3.1' +gem 'omniauth-auth0', '~> 1.4.1' gem 'omniauth-azure-oauth2', '~> 0.0.6' gem 'omniauth-bitbucket', '~> 0.0.2' gem 'omniauth-cas3', '~> 1.1.2' @@ -50,7 +51,7 @@ gem "browser", '~> 1.0.0' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '~> 9.0' +gem "gitlab_git", '~> 10.0' # LDAP Auth # GitLab fork with several improvements to original library. For full list of changes @@ -58,7 +59,9 @@ gem "gitlab_git", '~> 9.0' gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: "omniauth-ldap" # Git Wiki -gem 'gollum-lib', '~> 4.1.0' +# Required manually in config/initializers/gollum.rb to control load order +gem 'gollum-lib', '~> 4.1.0', require: false +gem 'gollum-rugged_adapter', '~> 0.4.2', require: false # Language detection gem "github-linguist", "~> 4.7.0", require: "linguist" diff --git a/Gemfile.lock b/Gemfile.lock index 9c785c903c0..e2dba76ce5e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -359,11 +359,11 @@ GEM posix-spawn (~> 0.3) gitlab_emoji (0.3.1) gemojione (~> 2.2, >= 2.2.1) - gitlab_git (9.0.3) + gitlab_git (10.0.0) activesupport (~> 4.0) charlock_holmes (~> 0.7.3) github-linguist (~> 4.7.0) - rugged (~> 0.24.0b13) + rugged (~> 0.24.0) gitlab_meta (7.0) gitlab_omniauth-ldap (1.2.1) net-ldap (~> 0.9) @@ -381,6 +381,9 @@ GEM rouge (~> 1.9) sanitize (~> 2.1.0) stringex (~> 2.5.1) + gollum-rugged_adapter (0.4.2) + mime-types (>= 1.15) + rugged (~> 0.24.0, >= 0.21.3) gon (6.0.1) actionpack (>= 3.0) json @@ -495,6 +498,8 @@ GEM omniauth (1.3.1) hashie (>= 1.2, < 4) rack (>= 1.0, < 3) + omniauth-auth0 (1.4.1) + omniauth-oauth2 (~> 1.1) omniauth-azure-oauth2 (0.0.6) jwt (~> 1.0) omniauth (~> 1.0) @@ -703,7 +708,7 @@ GEM rubyntlm (0.5.2) rubypants (0.2.0) rufus-scheduler (3.1.10) - rugged (0.24.0b13) + rugged (0.24.0) safe_yaml (1.0.4) sanitize (2.1.0) nokogiri (>= 1.4.4) @@ -937,10 +942,11 @@ DEPENDENCIES github-markup (~> 1.3.1) gitlab-flowdock-git-hook (~> 1.0.1) gitlab_emoji (~> 0.3.0) - gitlab_git (~> 9.0) + gitlab_git (~> 10.0) gitlab_meta (= 7.0) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.1.0) + gollum-rugged_adapter (~> 0.4.2) gon (~> 6.0.1) grape (~> 0.13.0) grape-entity (~> 0.4.2) @@ -969,6 +975,7 @@ DEPENDENCIES oauth2 (~> 1.0.0) octokit (~> 3.8.0) omniauth (~> 1.3.1) + omniauth-auth0 (~> 1.4.1) omniauth-azure-oauth2 (~> 0.0.6) omniauth-bitbucket (~> 0.0.2) omniauth-cas3 (~> 1.1.2) diff --git a/README.md b/README.md index 3ec1d4a776c..afa60116ebb 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ GitLab is a Ruby on Rails application that runs on the following software: - Ubuntu/Debian/CentOS/RHEL - Ruby (MRI) 2.1 -- Git 1.7.10+ +- Git 2.7.4+ - Redis 2.8+ - MySQL or PostgreSQL diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 1be86e3b820..f5e1ca9860d 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -14,7 +14,6 @@ class Dispatcher path = page.split(':') shortcut_handler = null - switch page when 'projects:issues:index' Issues.init() @@ -25,6 +24,8 @@ class Dispatcher new ZenMode() when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show' new Milestone() + when 'dashboard:todos:index' + new Todos() when 'projects:milestones:new', 'projects:milestones:edit' new ZenMode() new DropzoneInput($('.milestone-form')) diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 4f038477755..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/issuable_form.js.coffee b/app/assets/javascripts/issuable_form.js.coffee index 48c249943f2..6c1699c178c 100644 --- a/app/assets/javascripts/issuable_form.js.coffee +++ b/app/assets/javascripts/issuable_form.js.coffee @@ -1,4 +1,5 @@ class @IssuableForm + wipRegex: /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i constructor: (@form) -> GitLab.GfmAutoComplete.setup() new UsersSelect() @@ -14,6 +15,8 @@ class @IssuableForm @form.on "submit", @resetAutosave @form.on "click", ".btn-cancel", @resetAutosave + @initWip() + initAutosave: -> new Autosave @titleField, [ document.location.pathname, @@ -30,3 +33,41 @@ class @IssuableForm resetAutosave: => @titleField.data("autosave").reset() @descriptionField.data("autosave").reset() + + initWip: -> + @$wipExplanation = @form.find(".js-wip-explanation") + @$noWipExplanation = @form.find(".js-no-wip-explanation") + return unless @$wipExplanation.length and @$noWipExplanation.length + + @form.on "click", ".js-toggle-wip", @toggleWip + + @titleField.on "keyup blur", @renderWipExplanation + + @renderWipExplanation() + + workInProgress: -> + @wipRegex.test @titleField.val() + + renderWipExplanation: => + if @workInProgress() + @$wipExplanation.show() + @$noWipExplanation.hide() + else + @$wipExplanation.hide() + @$noWipExplanation.show() + + toggleWip: (event) => + event.preventDefault() + + if @workInProgress() + @removeWip() + else + @addWip() + + @renderWipExplanation() + + removeWip: -> + @titleField.val @titleField.val().replace(@wipRegex, "") + + addWip: -> + @titleField.val "WIP: #{@titleField.val()}" diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index d663e34871c..f50df1f5ea3 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -7,6 +7,7 @@ class @Issue # Prevent duplicate event bindings @disableTaskList() @fixAffixScroll() + @initParticipants() if $('a.btn-close').length @initTaskList() @initIssueBtnEventListeners() @@ -84,3 +85,27 @@ class @Issue type: 'PATCH' url: $('form.js-issuable-update').attr('action') data: patchData + + initParticipants: -> + _this = @ + $(document).on "click", ".js-participants-more", @toggleHiddenParticipants + + $(".js-participants-author").each (i) -> + if i >= _this.PARTICIPANTS_ROW_COUNT + $(@) + .addClass "js-participants-hidden" + .hide() + + toggleHiddenParticipants: (e) -> + e.preventDefault() + + currentText = $(this).text().trim() + lessText = $(this).data("less-text") + originalText = $(this).data("original-text") + + if currentText is originalText + $(this).text(lessText) + else + $(this).text(originalText) + + $(".js-participants-hidden").toggle() diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee index a0acf3028bf..1127b289264 100644 --- a/app/assets/javascripts/issues.js.coffee +++ b/app/assets/javascripts/issues.js.coffee @@ -41,24 +41,28 @@ @timer = null $("#issue_search").keyup -> clearTimeout(@timer) - @timer = setTimeout(Issues.filterResults, 500) + @timer = setTimeout( -> + Issues.filterResults $("#issue_search_form") + , 500) - filterResults: => - form = $("#issue_search_form") - search = $("#issue_search").val() - $('.issues-holder').css("opacity", '0.5') - issues_url = form.attr('action') + '?' + form.serialize() + filterResults: (form) => + $('.issues-holder, .merge-requests-holder').css("opacity", '0.5') + formAction = form.attr('action') + formData = form.serialize() + issuesUrl = formAction + issuesUrl += ("#{if formAction.indexOf("?") < 0 then '?' else '&'}") + issuesUrl += formData $.ajax type: "GET" - url: form.attr('action') - data: form.serialize() + url: formAction + data: formData complete: -> - $('.issues-holder').css("opacity", '1.0') + $('.issues-holder, .merge-requests-holder').css("opacity", '1.0') success: (data) -> - $('.issues-holder').html(data.html) + $('.issues-holder, .merge-requests-holder').html(data.html) # Change url so if user reload a page - search results are saved - history.replaceState {page: issues_url}, document.title, issues_url + history.replaceState {page: issuesUrl}, document.title, issuesUrl Issues.reload() dataType: "json" diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 5ade2cb66cb..4a0c18a99a6 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -1,30 +1,32 @@ class @LabelsSelect constructor: -> $('.js-label-select').each (i, dropdown) -> - projectId = $(dropdown).data('project-id') - labelUrl = $(dropdown).data("labels") - selectedLabel = $(dropdown).data('selected') + $dropdown = $(dropdown) + projectId = $dropdown.data('project-id') + labelUrl = $dropdown.data('labels') + selectedLabel = $dropdown.data('selected') if selectedLabel - selectedLabel = selectedLabel.split(",") + selectedLabel = selectedLabel.split(',') newLabelField = $('#new_label_name') newColorField = $('#new_label_color') - showNo = $(dropdown).data('show-no') - showAny = $(dropdown).data('show-any') + showNo = $dropdown.data('show-no') + showAny = $dropdown.data('show-any') + defaultLabel = $dropdown.data('default-label') if newLabelField.length - $('.suggest-colors-dropdown a').on "click", (e) -> + $('.suggest-colors-dropdown a').on 'click', (e) -> e.preventDefault() e.stopPropagation() - newColorField.val $(this).data("color") + newColorField.val $(this).data('color') $('.js-dropdown-label-color-preview') - .css 'background-color', $(this).data("color") + .css 'background-color', $(this).data('color') .addClass 'is-active' - $('.js-new-label-btn').on "click", (e) -> + $('.js-new-label-btn').on 'click', (e) -> e.preventDefault() e.stopPropagation() - if newLabelField.val() isnt "" && newColorField.val() isnt "" + if newLabelField.val() isnt '' and newColorField.val() isnt '' $('.js-new-label-btn').disable() # Create new label with API @@ -33,46 +35,38 @@ class @LabelsSelect color: newColorField.val() }, (label) -> $('.js-new-label-btn').enable() - $('.dropdown-menu-back', $(dropdown).parent()).trigger "click" + $('.dropdown-menu-back', $dropdown.parent()).trigger 'click' - $(dropdown).glDropdown( + $dropdown.glDropdown( data: (term, callback) -> - # We have to fetch the JS version of the labels list because there is no - # public facing JSON url for labels $.ajax( url: labelUrl ).done (data) -> - html = $(data) - data = [] - html.find('.label-row a').each -> - data.push( - title: $(@).text().trim() - ) - if showNo data.unshift( - id: "0" - title: 'No label' + id: 0 + title: 'No Label' ) if showAny data.unshift( - title: 'Any label' + isAny: true + title: 'Any Label' ) if data.length > 2 - data.splice 2, 0, "divider" + data.splice 2, 0, 'divider' callback data renderRow: (label) -> if $.isArray(selectedLabel) - selected = "" + selected = '' $.each selectedLabel, (i, selectedLbl) -> selectedLbl = selectedLbl.trim() - if selected is "" && label.title is selectedLbl - selected = "is-active" + if selected is '' and label.title is selectedLbl + selected = 'is-active' else - selected = if label.title is selectedLabel then "is-active" else "" + selected = if label.title is selectedLabel then 'is-active' else '' "<li> <a href='#' class='#{selected}'> @@ -83,10 +77,24 @@ class @LabelsSelect search: fields: ['title'] selectable: true - fieldName: $(dropdown).data('field-name') + toggleLabel: (selected) -> + if selected and selected.title isnt 'Any Label' + selected.title + else + defaultLabel + fieldName: $dropdown.data('field-name') id: (label) -> - label.title + if label.isAny? + '' + else + label.title clicked: -> - if $(dropdown).hasClass "js-filter-submit" - $(dropdown).parents('form').submit() + page = $('body').data 'page' + isIssueIndex = page is 'projects:issues:index' + isMRIndex = page is page is 'projects:merge_requests:index' + + if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) + Issues.filterResults $dropdown.closest('form') + else if $dropdown.hasClass 'js-filter-submit' + $dropdown.closest('form').submit() ) diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee index 5e884454a65..e17a1adb648 100644 --- a/app/assets/javascripts/milestone_select.js.coffee +++ b/app/assets/javascripts/milestone_select.js.coffee @@ -1,60 +1,65 @@ class @MilestoneSelect constructor: -> $('.js-milestone-select').each (i, dropdown) -> - projectId = $(dropdown).data('project-id') - milestonesUrl = $(dropdown).data('milestones') - selectedMilestone = $(dropdown).data('selected') - showNo = $(dropdown).data('show-no') - showAny = $(dropdown).data('show-any') - useId = $(dropdown).data('use-id') + $dropdown = $(dropdown) + projectId = $dropdown.data('project-id') + milestonesUrl = $dropdown.data('milestones') + selectedMilestone = $dropdown.data('selected') + showNo = $dropdown.data('show-no') + showAny = $dropdown.data('show-any') + useId = $dropdown.data('use-id') + defaultLabel = $dropdown.data('default-label') - $(dropdown).glDropdown( + $dropdown.glDropdown( data: (term, callback) -> $.ajax( url: milestonesUrl ).done (data) -> - html = $(data) - data = [] - html.find('.milestone strong a').each -> - link = $(@).attr("href").split("/") - data.push( - id: link[link.length - 1] - title: $(@).text().trim() - ) - if showNo data.unshift( - id: "0" + id: '0' title: 'No Milestone' ) if showAny data.unshift( + isAny: true title: 'Any Milestone' ) if data.length > 2 - data.splice 2, 0, "divider" + data.splice 2, 0, 'divider' callback(data) filterable: true search: fields: ['title'] selectable: true - fieldName: $(dropdown).data('field-name') + toggleLabel: (selected) -> + if selected && 'id' of selected + selected.title + else + defaultLabel + fieldName: $dropdown.data('field-name') text: (milestone) -> milestone.title id: (milestone) -> if !useId - if milestone.title isnt "Any milestone" + if !milestone.isAny? milestone.title else - "" + '' else milestone.id isSelected: (milestone) -> milestone.title is selectedMilestone clicked: -> - if $(dropdown).hasClass "js-filter-submit" - $(dropdown).parents('form').submit() + page = $('body').data 'page' + isIssueIndex = page is 'projects:issues:index' + isMRIndex = page is page is 'projects:merge_requests:index' + + if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex) + Issues.filterResults $dropdown.closest('form') + else if $dropdown.hasClass 'js-filter-submit' + $dropdown.closest('form').submit() ) diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 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 31084872367..2fac6abbac9 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -23,15 +23,11 @@ margin-bottom: -$gl-padding; background-color: $background-color; padding: $gl-padding; - margin-bottom: 0px; + margin-bottom: 0; border-top: 1px solid $border-color; 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 8d475137b03..657c5f033c7 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -37,23 +37,23 @@ } @mixin btn-green { - @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, #FFFFFF); + @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, #fff); } @mixin btn-blue { - @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, #FFFFFF); + @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, #fff); } @mixin btn-blue-medium { - @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, #FFFFFF); + @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, #fff); } @mixin btn-orange { - @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, #FFFFFF); + @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, #fff); } @mixin btn-red { - @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, #FFFFFF); + @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, #fff); } @mixin btn-gray { @@ -127,7 +127,7 @@ margin-right: 7px; float: left; &:last-child { - margin-right: 0px; + margin-right: 0; } &.btn-xs { margin-right: 3px; @@ -139,7 +139,6 @@ .caret { margin-left: 5px; - color: $gray-darkest; } } @@ -170,7 +169,7 @@ margin-right: 7px; float: left; &:last-child { - margin-right: 0px; + margin-right: 0; } } } @@ -209,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/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index 580012abd77..e3192823a1a 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -33,19 +33,19 @@ } .q2 { - fill: #ACD5F2 !important; + fill: #acd5f2 !important; } .q3 { - fill: #7FA8D1 !important; + fill: #7fa8d1 !important; } .q4 { - fill: #49729B !important; + fill: #49729b !important; } .q5 { - fill: #254E77 !important; + fill: #254e77 !important; } .domain-background { diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss index 20a9bfb9816..da7bab74a32 100644 --- a/app/assets/stylesheets/framework/callout.scss +++ b/app/assets/stylesheets/framework/callout.scss @@ -39,6 +39,6 @@ } .bs-callout-success { background-color: #dff0d8; - border-color: #5cA64d; + border-color: #5ca64d; color: #3c763d; } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index ff551f151f1..bc03c2180be 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -1,6 +1,6 @@ /** COLORS **/ .cgray { color: $gl-gray; } -.clgray { color: #BBB } +.clgray { color: #bbb } .cred { color: $gl-text-red; } .cgreen { color: $gl-text-green; } .cdark { color: #444 } @@ -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 } @@ -51,7 +51,7 @@ pre { } &.well-pre { - border: 1px solid #EEE; + border: 1px solid #eee; background: #f9f9f9; border-radius: 0; color: #555; @@ -103,7 +103,7 @@ span.update-author { } .user-mention { - color: #2FA0BB; + color: #2fa0bb; font-weight: bold; } @@ -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; } } } @@ -187,9 +187,9 @@ li.note { .error-message { padding: 10px; - background: #C67; + background: #c67; margin: 0; - color: #FFF; + color: #fff; a { color: #fff; @@ -200,7 +200,7 @@ li.note { .browser-alert { padding: 10px; text-align: center; - background: #C67; + background: #c67; color: #fff; font-weight: bold; a { @@ -271,7 +271,7 @@ img.emoji { table { td.permission-x { - background: #D9EDF7 !important; + background: #d9edf7 !important; text-align: center; } } @@ -280,7 +280,7 @@ table { float: left; text-align: center; font-size: 32px; - color: #AAA; + color: #aaa; width: 60px; } @@ -347,7 +347,7 @@ table { .profiler-button, .profiler-controls { - border-color: #EEE !important; + border-color: #eee !important; } } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 5b647fc6176..a48b6c17fa0 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -4,11 +4,17 @@ height: 0; margin-left: 2px; vertical-align: middle; - border-top: $caret-width-base dashed $dropdown-caret-color; + border-top: $caret-width-base dashed; border-right: $caret-width-base solid transparent; border-left: $caret-width-base solid transparent; } +.btn-group { + .caret { + margin-left: 0; + } +} + .dropdown { position: relative; } @@ -161,9 +167,8 @@ .dropdown-menu-user-full-name { display: block; - margin-bottom: 2px; font-weight: 600; - line-height: 1; + line-height: 16px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; @@ -171,7 +176,7 @@ .dropdown-menu-user-username { display: block; - line-height: 1; + line-height: 16px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; @@ -261,7 +266,7 @@ position: absolute; top: 10px; right: 10px; - color: #C7C7C7; + color: #c7c7c7; font-size: 12px; pointer-events: none; } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index b034a4882c1..646e2610831 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -30,7 +30,7 @@ right: 15px; .btn { - padding: 0px 10px; + padding: 0 10px; font-size: 13px; line-height: 28px; } @@ -84,7 +84,7 @@ &.blob-no-preview { background: #eee; - text-shadow: 0 1px 2px #FFF; + text-shadow: 0 1px 2px #fff; padding: 100px 0; } @@ -124,7 +124,7 @@ } td.line-numbers { float: none; - border-left: 1px solid #DDD; + border-left: 1px solid #ddd; } td.lines { padding: 0; 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/forms.scss b/app/assets/stylesheets/framework/forms.scss index 6c08005812e..4cb4129b71b 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -41,8 +41,8 @@ input[type='search'].search-input { } input[type='text'].danger { - background: #F2DEDE!important; - border-color: #D66; + background: #f2dede!important; + border-color: #d66; text-shadow: 0 1px 1px #fff } diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss index 12cef6f8ea1..2a4cf4fc335 100644 --- a/app/assets/stylesheets/framework/gitlab-theme.scss +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -23,13 +23,13 @@ &:hover { background-color: $color-darker; a { - color: #FFF; + color: #fff; } } } .collapse-nav a { - color: #FFF; + color: #fff; background: $color; } @@ -42,7 +42,7 @@ &:hover { background-color: $color-dark; - color: #FFF; + color: #fff; text-decoration: none; } } @@ -71,7 +71,7 @@ } &.active a { - color: #FFF; + color: #fff; background: $color-dark; &.no-highlight { @@ -79,42 +79,42 @@ } i { - color: #FFF + color: #fff } } } } } -$theme-blue: #2980B9; +$theme-blue: #2980b9; $theme-charcoal: #333c47; -$theme-graphite: #888888; +$theme-graphite: #888; $theme-gray: #373737; $theme-green: #019875; -$theme-violet: #554488; +$theme-violet: #548; body { &.ui_blue { - @include gitlab-theme(#BECDE9, $theme-blue, #1970A9, #096099); + @include gitlab-theme(#becde9, $theme-blue, #1970a9, #096099); } &.ui_charcoal { - @include gitlab-theme(#c5d0de, $theme-charcoal, #2b333d, #24272D); + @include gitlab-theme(#c5d0de, $theme-charcoal, #2b333d, #24272d); } &.ui_graphite { - @include gitlab-theme(#CCCCCC, $theme-graphite, #777777, #666666); + @include gitlab-theme(#ccc, $theme-graphite, #777, #666); } &.ui_gray { - @include gitlab-theme(#979797, $theme-gray, #272727, #222222); + @include gitlab-theme(#979797, $theme-gray, #272727, #222); } &.ui_green { - @include gitlab-theme(#AADDCC, $theme-green, #018865, #017855); + @include gitlab-theme(#adc, $theme-green, #018865, #017855); } &.ui_violet { - @include gitlab-theme(#9988CC, $theme-violet, #443366, #332255); + @include gitlab-theme(#98c, $theme-violet, #436, #325); } }
\ No newline at end of file diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 4c4033e3ae7..71a7ecab8ef 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -7,8 +7,8 @@ header { &.navbar-empty { height: 58px; - background: #FFF; - border-bottom: 1px solid #EEE; + background: #fff; + border-bottom: 1px solid #eee; .center-logo { margin: 11px 0; @@ -28,7 +28,7 @@ header { min-height: $header-height; background-color: #fff; border: none; - border-bottom: 1px solid #EEE; + border-bottom: 1px solid #eee; .container-fluid { width: 100% !important; @@ -47,7 +47,7 @@ header { text-align: center; &:hover, &:focus, &:active { - background-color: #FFF; + background-color: #fff; } } @@ -59,7 +59,7 @@ header { right: 2px; &:hover { - background-color: #EEE; + background-color: #eee; } &.active { color: #7f8fa4; @@ -162,7 +162,7 @@ header { font-size: 18px; .navbar-nav { - margin: 0px; + margin: 0; float: none !important; .visible-xs, .visable-sm { diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss index 12e2f00fe89..7cf4d4fba42 100644 --- a/app/assets/stylesheets/framework/highlight.scss +++ b/app/assets/stylesheets/framework/highlight.scss @@ -1,8 +1,8 @@ .file-content.code { border: none; box-shadow: none; - margin: 0px; - padding: 0px; + margin: 0; + padding: 0; table-layout: fixed; pre { diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index 77a00586b26..7f7b7c806e7 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -20,7 +20,7 @@ display: block; float: left; margin-right: 10px; - color: #FFF; + color: #fff; font-size: $gl-font-size; line-height: 25px; diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss index 0cdcd923b3c..525ed81b059 100644 --- a/app/assets/stylesheets/framework/jquery.scss +++ b/app/assets/stylesheets/framework/jquery.scss @@ -3,13 +3,13 @@ font-size: $font-size-base; &.ui-datepicker-inline { - border: 1px solid #DDD; + border: 1px solid #ddd; padding: 10px; width: 270px; .ui-datepicker-header { - background: #FFF; - border-color: #DDD; + background: #fff; + border-color: #ddd; } .ui-datepicker-calendar td a { @@ -19,7 +19,7 @@ } &.ui-autocomplete { - border-color: #DDD; + border-color: #ddd; padding: 0; margin-top: 2px; z-index: 1001; @@ -30,20 +30,20 @@ } .ui-state-default { - border: 1px solid #FFF; - background: #FFF; + border: 1px solid #fff; + background: #fff; color: #777; } .ui-state-highlight { - border: 1px solid #EEE; - background: #EEE; + border: 1px solid #eee; + background: #eee; } .ui-state-active { border: 1px solid $gl-primary; background: $gl-primary; - color: #FFF; + color: #fff; } .ui-state-hover, 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/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 1d8611b04dc..8328aac4e7a 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -71,7 +71,7 @@ } .md-preview-holder { - background: #FFF; + background: #fff; border: 1px solid #ddd; min-height: 169px; padding: 5px; @@ -80,7 +80,7 @@ .markdown-area { @include border-radius(0); - background: #FFF; + background: #fff; border: 1px solid #ddd; min-height: 140px; max-height: 500px; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 1d5000fe388..377bfa174bd 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -67,17 +67,17 @@ * Base mixin for lists in GitLab */ @mixin basic-list { - margin: 5px 0px; - padding: 0px; + margin: 5px 0; + padding: 0; list-style: none; > li { @include clearfix; padding: 10px 0; - border-bottom: 1px solid #EEE; + border-bottom: 1px solid #eee; display: block; - margin: 0px; + margin: 0; &:last-child { border-bottom: none; diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index d088228fe4c..bf874b9b822 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -128,12 +128,12 @@ .show-aside { display: none; position: fixed; - right: 0px; + right: 0; top: 30%; padding: 5px 15px; - background: #EEE; + background: #eee; font-size: 20px; color: #777; z-index: 100; - @include box-shadow(0 1px 2px #DDD); + @include box-shadow(0 1px 2px #ddd); } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index b2fbc95e043..5f4ce87b085 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -26,7 +26,7 @@ } &.active a { - color: #000000; + color: #000; border-bottom: 2px solid #4688f1; } @@ -41,7 +41,7 @@ .top-area { @include clearfix; - border-bottom: 1px solid #EEE; + border-bottom: 1px solid #eee; .nav-text { padding-top: 16px; @@ -59,7 +59,7 @@ .nav-links { display: inline-block; width: 50%; - margin-bottom: 0px; + margin-bottom: 0; border-bottom: none; /* Small devices (phones, tablets, 768px and lower) */ @@ -74,7 +74,7 @@ float: right; text-align: right; padding: 11px 0; - margin-bottom: 0px; + margin-bottom: 0; > .dropdown { margin-right: $gl-padding-top; diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 7bf04e4ad74..b3371229d5a 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -151,7 +151,7 @@ padding: 2px 25px 2px 5px; background: #fff image-url('select2.png'); background-repeat: no-repeat; - background-position: right 0px bottom 6px; + background-position: right 0 bottom 6px; border: 1px solid $input-border; @include border-radius($border-radius-default); @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s); @@ -229,7 +229,7 @@ .namespace-result { .namespace-kind { - color: #AAA; + color: #aaa; font-weight: normal; } .namespace-path { diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 26df9acd2ae..be05db58c40 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -16,7 +16,7 @@ .gitlab-text-container-link { z-index: 1; position: absolute; - left: 0px; + left: 0; } #logo { @@ -47,7 +47,7 @@ width: 100%; .container-fluid { - background: #FFF; + background: #fff; padding: 0 $gl-padding; &.container-blank { @@ -103,7 +103,7 @@ } &:hover { - background-color: #EEE; + background-color: #eee; } } @@ -143,7 +143,7 @@ overflow: hidden; &.navbar-collapse { - padding: 0px !important; + padding: 0 !important; } li { @@ -182,7 +182,7 @@ .count { float: right; background: #eee; - padding: 0px 8px; + padding: 0 8px; @include border-radius(6px); } @@ -194,8 +194,8 @@ } .sidebar-subnav { - margin-left: 0px; - padding-left: 0px; + margin-left: 0; + padding-left: 0; li { list-style: none; diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index ddf76704a53..dd42db1840f 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -95,7 +95,7 @@ } &.label-inverse { - background-color: #333333; + background-color: #333; } } @@ -138,7 +138,7 @@ } .btn-clipboard { - min-width: 0px; + min-width: 0; } } diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index b1b8295411b..f63ac033234 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -57,7 +57,7 @@ $component-active-bg: $brand-info; $input-color: $text-color; $input-border: #e7e9ed; -$input-border-focus: #7F8FA4; +$input-border-focus: #7f8fa4; $legend-color: $text-color; @@ -125,8 +125,8 @@ $panel-inner-border: $border-color; // //## -$well-bg: #F9F9F9; -$well-border: #EEE; +$well-bg: #f9f9f9; +$well-border: #eee; //== Code // diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 9381cb3281c..949295a1d0c 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -27,13 +27,13 @@ line-height: 10px; color: #555; vertical-align: middle; - background-color: #FCFCFC; + background-color: #fcfcfc; border-width: 1px; border-style: solid; - border-color: #CCC #CCC #BBB; + border-color: #ccc #ccc #bbb; border-image: none; border-radius: 3px; - box-shadow: 0px -1px 0px #BBB inset; + box-shadow: 0 -1px 0 #bbb inset; } h1 { @@ -187,7 +187,7 @@ body { } .page-title-empty { - margin-top: 0px; + margin-top: 0; line-height: 1.3; font-size: 1.25em; font-weight: 600; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index d491d01a3cf..be626678bd7 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -1,53 +1,83 @@ -$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; +$error-exclamation-point: #e62958; $border-radius-default: 3px; -$list-title-color: #333333; -$list-text-color: #555555; - -$btn-transparent-color: #8F8F8F; - -$ssh-key-icon-color: #8F8F8F; +$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; +$provider-btn-group-border: #e5e5e5; +$provider-btn-not-active-color: #4688f1; /* * Color schema */ -$white-light: #FFFFFF; +$white-light: #fff; $white-normal: #ededed; $white-dark: #ededed; @@ -57,55 +87,55 @@ $gray-dark: #ededed; $gray-darkest: #c9c9c9; $green-light: #38ae67; -$green-normal: #2FAA60; -$green-dark: #2CA05B; +$green-normal: #2faa60; +$green-dark: #2ca05b; -$blue-light: #2EA8E5; -$blue-normal: #2D9FD8; -$blue-dark: #2897CE; +$blue-light: #2ea8e5; +$blue-normal: #2d9fd8; +$blue-dark: #2897ce; -$blue-medium-light: #3498CB; -$blue-medium: #2F8EBF; -$blue-medium-dark: #2D86B4; +$blue-medium-light: #3498cb; +$blue-medium: #2f8ebf; +$blue-medium-dark: #2d86b4; $orange-light: rgba(252, 109, 38, 0.80); -$orange-normal: #E75E40; -$orange-dark: #CE5237; +$orange-normal: #e75e40; +$orange-dark: #ce5237; -$red-light: #F06559; -$red-normal: #E52C5A; -$red-dark: #D22852; +$red-light: #f06559; +$red-normal: #e52c5a; +$red-dark: #d22852; -$border-white-light: #F1F2F4; -$border-white-normal: #D6DAE2; -$border-white-dark: #C6CACF; +$border-white-light: #f1f2f4; +$border-white-normal: #d6dae2; +$border-white-dark: #c6cacf; $border-gray-light: rgba(0, 0, 0, 0.06); $border-gray-normal: rgba(0, 0, 0, 0.10);; -$border-gray-dark: #C6CACF; +$border-gray-dark: #c6cacf; -$border-green-light: #2FAA60; -$border-green-normal: #2CA05B; +$border-green-light: #2faa60; +$border-green-normal: #2ca05b; $border-green-dark: #279654; -$border-blue-light: #2D9FD8; -$border-blue-normal: #2897CE; -$border-blue-dark: #258DC1; +$border-blue-light: #2d9fd8; +$border-blue-normal: #2897ce; +$border-blue-dark: #258dc1; $border-orange-light: #fc6d26; -$border-orange-normal: #CE5237; -$border-orange-dark: #C14E35; +$border-orange-normal: #ce5237; +$border-orange-dark: #c14e35; -$border-red-light: #F24F41; -$border-red-normal: #D22852; -$border-red-dark: #CA264F; +$border-red-light: #f24f41; +$border-red-normal: #d22852; +$border-red-dark: #ca264f; -$help-well-bg: #FAFAFA; -$help-well-border: #E5E5E5; +$help-well-bg: #fafafa; +$help-well-border: #e5e5e5; -$warning-message-bg: #FBF2D9; -$warning-message-color: #9E8E60; -$warning-message-border: #F0E2BB; +$warning-message-bg: #fbf2d9; +$warning-message-color: #9e8e60; +$warning-message-border: #f0e2bb; /* header */ $light-grey-header: #faf9f9; @@ -143,23 +173,22 @@ $dropdown-border-color: rgba(#000, .1); $dropdown-shadow-color: rgba(#000, .1); $dropdown-divider-color: rgba(#000, .1); $dropdown-header-color: #959494; -$dropdown-caret-color: #54565B; -$dropdown-title-btn-color: #BFBFBF; -$dropdown-input-color: #C7C7C7; +$dropdown-title-btn-color: #bfbfbf; +$dropdown-input-color: #c7c7c7; $dropdown-input-focus-border: rgb(58, 171, 240); $dropdown-input-focus-shadow: rgba(#000, .2); $dropdown-loading-bg: rgba(#fff, .6); $dropdown-toggle-bg: #fff; $dropdown-toggle-color: #626262; -$dropdown-toggle-border-color: #EAEAEA; +$dropdown-toggle-border-color: #eaeaea; $dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 15%); -$dropdown-toggle-icon-color: #C4C4C4; +$dropdown-toggle-icon-color: #c4c4c4; $dropdown-toggle-hover-icon-color: $dropdown-toggle-hover-border-color; /* * Award emoji */ -$award-emoji-menu-bg: #FFF; -$award-emoji-menu-border: #F1F2F4; -$award-emoji-new-btn-icon-color: #DCDCDC; +$award-emoji-menu-bg: #fff; +$award-emoji-menu-border: #f1f2f4; +$award-emoji-new-btn-icon-color: #dcdcdc; diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index c3f27333fad..02e24ec7c4d 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -2,7 +2,7 @@ a.js-zen-enter { color: $gl-gray; position: absolute; - top: 0px; + top: 0; right: 4px; line-height: 56px; } diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index b794da2ce98..47673944896 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -43,12 +43,12 @@ // Search result highlight span.highlight_word { background-color: #ffe792 !important; - color: #000000 !important; + color: #000 !important; } .hll { background-color: #373b41 } .c { color: #969896 } /* Comment */ - .err { color: #cc6666 } /* Error */ + .err { color: #c66 } /* Error */ .k { color: #b294bb } /* Keyword */ .l { color: #de935f } /* Literal */ .n { color: #c5c8c6 } /* Name */ @@ -58,7 +58,7 @@ .cp { color: #969896 } /* Comment.Preproc */ .c1 { color: #969896 } /* Comment.Single */ .cs { color: #969896 } /* Comment.Special */ - .gd { color: #cc6666 } /* Generic.Deleted */ + .gd { color: #c66 } /* Generic.Deleted */ .ge { font-style: italic } /* Generic.Emph */ .gh { color: #c5c8c6; font-weight: bold } /* Generic.Heading */ .gi { color: #b5bd68 } /* Generic.Inserted */ @@ -77,17 +77,17 @@ .na { color: #81a2be } /* Name.Attribute */ .nb { color: #c5c8c6 } /* Name.Builtin */ .nc { color: #f0c674 } /* Name.Class */ - .no { color: #cc6666 } /* Name.Constant */ + .no { color: #c66 } /* Name.Constant */ .nd { color: #8abeb7 } /* Name.Decorator */ .ni { color: #c5c8c6 } /* Name.Entity */ - .ne { color: #cc6666 } /* Name.Exception */ + .ne { color: #c66 } /* Name.Exception */ .nf { color: #81a2be } /* Name.Function */ .nl { color: #c5c8c6 } /* Name.Label */ .nn { color: #f0c674 } /* Name.Namespace */ .nx { color: #81a2be } /* Name.Other */ .py { color: #c5c8c6 } /* Name.Property */ .nt { color: #8abeb7 } /* Name.Tag */ - .nv { color: #cc6666 } /* Name.Variable */ + .nv { color: #c66 } /* Name.Variable */ .ow { color: #8abeb7 } /* Operator.Word */ .w { color: #c5c8c6 } /* Text.Whitespace */ .mf { color: #de935f } /* Literal.Number.Float */ @@ -106,8 +106,8 @@ .s1 { color: #b5bd68 } /* Literal.String.Single */ .ss { color: #b5bd68 } /* Literal.String.Symbol */ .bp { color: #c5c8c6 } /* Name.Builtin.Pseudo */ - .vc { color: #cc6666 } /* Name.Variable.Class */ - .vg { color: #cc6666 } /* Name.Variable.Global */ - .vi { color: #cc6666 } /* Name.Variable.Instance */ + .vc { color: #c66 } /* Name.Variable.Class */ + .vg { color: #c66 } /* Name.Variable.Global */ + .vi { color: #c66 } /* Name.Variable.Instance */ .il { color: #de935f } /* Literal.Number.Integer.Long */ } diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index 9098e07adcd..806401c21ae 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -43,7 +43,7 @@ // Search result highlight span.highlight_word { background-color: #ffe792 !important; - color: #000000 !important; + color: #000 !important; } .hll { background-color: #49483e } diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index 8b1a2824f76..6a809d4dfd2 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -96,7 +96,7 @@ .m { color: #2aa198 } /* Literal.Number */ .s { color: #2aa198 } /* Literal.String */ .na { color: #93a1a1 } /* Name.Attribute */ - .nb { color: #B58900 } /* Name.Builtin */ + .nb { color: #b58900 } /* Name.Builtin */ .nc { color: #268bd2 } /* Name.Class */ .no { color: #cb4b16 } /* Name.Constant */ .nd { color: #268bd2 } /* Name.Decorator */ diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index 7ad89dd2c7c..b90c95c62d1 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -96,7 +96,7 @@ .m { color: #2aa198 } /* Literal.Number */ .s { color: #2aa198 } /* Literal.String */ .na { color: #586e75 } /* Name.Attribute */ - .nb { color: #B58900 } /* Name.Builtin */ + .nb { color: #b58900 } /* Name.Builtin */ .nc { color: #268bd2 } /* Name.Class */ .no { color: #cb4b16 } /* Name.Constant */ .nd { color: #268bd2 } /* Name.Decorator */ diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 8a091028a6c..8c1b0cd84ec 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -23,7 +23,7 @@ .line_holder { .diff-line-num { &.old { - background: #ffdddd; + background: #fdd; border-color: #f1c0c0; } @@ -68,66 +68,66 @@ } .hll { background-color: #f8f8f8 } - .c { color: #999988; font-style: italic; } + .c { color: #998; font-style: italic; } .err { color: #a61717; background-color: #e3d2d2; } .k { font-weight: bold; } .o { font-weight: bold; } - .cm { color: #999988; font-style: italic; } - .cp { color: #999999; font-weight: bold; } - .c1 { color: #999988; font-style: italic; } - .cs { color: #999999; font-weight: bold; font-style: italic; } - .gd { color: #000000; background-color: #ffdddd; } - .gd .x { color: #000000; background-color: #ffaaaa; } + .cm { color: #998; font-style: italic; } + .cp { color: #999; font-weight: bold; } + .c1 { color: #998; font-style: italic; } + .cs { color: #999; font-weight: bold; font-style: italic; } + .gd { color: #000; background-color: #fdd; } + .gd .x { color: #000; background-color: #faa; } .ge { font-style: italic; } - .gr { color: #aa0000; } - .gh { color: #999999; } - .gi { color: #000000; background-color: #ddffdd; } - .gi .x { color: #000000; background-color: #aaffaa; } - .go { color: #888888; } - .gp { color: #555555; } + .gr { color: #a00; } + .gh { color: #999; } + .gi { color: #000; background-color: #dfd; } + .gi .x { color: #000; background-color: #afa; } + .go { color: #888; } + .gp { color: #555; } .gs { font-weight: bold; } .gu { color: #800080; font-weight: bold; } - .gt { color: #aa0000; } + .gt { color: #a00; } .kc { font-weight: bold; } .kd { font-weight: bold; } .kn { font-weight: bold; } .kp { font-weight: bold; } .kr { font-weight: bold; } - .kt { color: #445588; font-weight: bold; } - .m { color: #009999; } - .s { color: #dd1144; } - .n { color: #333333; } + .kt { color: #458; font-weight: bold; } + .m { color: #099; } + .s { color: #d14; } + .n { color: #333; } .na { color: teal; } .nb { color: #0086b3; } - .nc { color: #445588; font-weight: bold; } + .nc { color: #458; font-weight: bold; } .no { color: teal; } .ni { color: purple; } - .ne { color: #990000; font-weight: bold; } - .nf { color: #990000; font-weight: bold; } - .nn { color: #555555; } + .ne { color: #900; font-weight: bold; } + .nf { color: #900; font-weight: bold; } + .nn { color: #555; } .nt { color: navy; } .nv { color: teal; } .ow { font-weight: bold; } - .w { color: #bbbbbb; } - .mf { color: #009999; } - .mh { color: #009999; } - .mi { color: #009999; } - .mo { color: #009999; } - .sb { color: #dd1144; } - .sc { color: #dd1144; } - .sd { color: #dd1144; } - .s2 { color: #dd1144; } - .se { color: #dd1144; } - .sh { color: #dd1144; } - .si { color: #dd1144; } - .sx { color: #dd1144; } + .w { color: #bbb; } + .mf { color: #099; } + .mh { color: #099; } + .mi { color: #099; } + .mo { color: #099; } + .sb { color: #d14; } + .sc { color: #d14; } + .sd { color: #d14; } + .s2 { color: #d14; } + .se { color: #d14; } + .sh { color: #d14; } + .si { color: #d14; } + .sx { color: #d14; } .sr { color: #009926; } - .s1 { color: #dd1144; } + .s1 { color: #d14; } .ss { color: #990073; } - .bp { color: #999999; } + .bp { color: #999; } .vc { color: teal; } .vg { color: teal; } .vi { color: teal; } - .il { color: #009999; } - .gc { color: #999; background-color: #EAF2F5; } + .il { color: #099; } + .gc { color: #999; background-color: #eaf2f5; } } 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/builds.scss b/app/assets/stylesheets/pages/builds.scss index 75f298019e3..201f3e5ca46 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -1,6 +1,6 @@ .build-page { pre.trace { - background: #111111; + background: #111; color: #fff; font-family: $monospace_font; white-space: pre; diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index c0cc30d33a6..971656feb42 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -55,7 +55,7 @@ padding: 10px 0; li { - padding: 3px 0px; + padding: 3px 0; line-height: 20px; } } diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 818fd03e2ae..33b3c7558ed 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -9,7 +9,7 @@ .lists-separator { margin: 10px 0; - border-color: #DDD; + border-color: #ddd; } .commits-row { @@ -55,7 +55,7 @@ li.commit { } .commit-row-message { - color: $gl-link-color; + color: $gl-dark-link-color; &:hover { text-decoration: underline; @@ -76,7 +76,7 @@ li.commit { .commit-row-description { font-size: 14px; - border-left: 1px solid #EEE; + border-left: 1px solid #eee; padding: 10px 15px; margin: 5px 0 10px 5px; background: #f9f9f9; @@ -152,7 +152,7 @@ li.commit { .count { padding-top: 6px; - padding-bottom: 0px; + padding-bottom: 0; font-size: 12px; color: #333; display: block; 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 a7925e79549..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; @@ -29,7 +29,7 @@ .diff-content { overflow: auto; overflow-y: hidden; - background: #FFF; + background: #fff; color: #333; .unfold { @@ -57,8 +57,8 @@ font-family: $monospace_font; border: none; border-collapse: separate; - margin: 0px; - padding: 0px; + margin: 0; + padding: 0; .line_holder td { line-height: $code_line_height; font-size: $code_font_size; @@ -76,10 +76,10 @@ } .old_line, .new_line { - margin: 0px; - padding: 0px; + margin: 0; + padding: 0; border: none; - padding: 0px 5px; + padding: 0 5px; border-right: 1px solid; text-align: right; min-width: 35px; @@ -97,8 +97,8 @@ } .line_content { display: block; - margin: 0px; - padding: 0px 0.5em; + margin: 0; + padding: 0 0.5em; border: none; &.parallel { display: table-cell; @@ -118,7 +118,7 @@ background-color: #fff; line-height: 0; img { - border: 1px solid #FFF; + border: 1px solid #fff; background: image-url('trans_bg.gif'); max-width: 100%; } @@ -183,7 +183,7 @@ height: 14px; width: 15px; position: absolute; - top: 0px; + top: 0; background: image-url('swipemode_sprites.gif') 0 3px no-repeat; } .bottom-handle { @@ -191,7 +191,7 @@ height: 14px; width: 15px; position: absolute; - bottom: 0px; + bottom: 0; background: image-url('swipemode_sprites.gif') 0 -11px no-repeat; } } @@ -206,8 +206,8 @@ .frame.added, .frame.deleted { position: absolute; display: block; - top: 0px; - left: 0px; + top: 0; + left: 0; } .controls { display: block; @@ -215,7 +215,7 @@ width: 300px; z-index: 100; position: absolute; - bottom: 0px; + bottom: 0; left: 50%; margin-left: -150px; @@ -231,11 +231,11 @@ .dragger { display: block; position: absolute; - left: 0px; - top: 0px; + left: 0; + top: 0; height: 14px; width: 14px; - background: image-url('onion_skin_sprites.gif') 0px -34px repeat-x; + background: image-url('onion_skin_sprites.gif') 0 -34px repeat-x; cursor: pointer; } @@ -243,17 +243,17 @@ display: block; position: absolute; top: 2px; - right: 0px; + right: 0; height: 10px; width: 10px; - background: image-url('onion_skin_sprites.gif') -2px 0px no-repeat; + background: image-url('onion_skin_sprites.gif') -2px 0 no-repeat; } .opaque { display: block; position: absolute; top: 2px; - left: 0px; + left: 0; height: 10px; width: 10px; background: image-url('onion_skin_sprites.gif') -2px -10px no-repeat; @@ -265,7 +265,7 @@ .view-modes { padding: 10px; text-align: center; - background: #EEE; + background: #eee; ul, li { list-style: none; @@ -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/editor.scss b/app/assets/stylesheets/pages/editor.scss index 39d916cd336..43be5e38ba8 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -14,9 +14,9 @@ } .cancel-btn { - color: #B94A48; + color: #b94a48; &:hover { - color: #B94A48; + color: #b94a48; } } diff --git a/app/assets/stylesheets/pages/emojis.scss b/app/assets/stylesheets/pages/emojis.scss index 6c721b514f8..b731abc7450 100644 --- a/app/assets/stylesheets/pages/emojis.scss +++ b/app/assets/stylesheets/pages/emojis.scss @@ -1,60 +1,60 @@ -.emoji-0023-20E3 { background-position: 0px 0px; } -.emoji-002A-20E3 { background-position: -20px 0px; } -.emoji-0030-20E3 { background-position: 0px -20px; } +.emoji-0023-20E3 { background-position: 0 0; } +.emoji-002A-20E3 { background-position: -20px 0; } +.emoji-0030-20E3 { background-position: 0 -20px; } .emoji-0031-20E3 { background-position: -20px -20px; } -.emoji-0032-20E3 { background-position: -40px 0px; } +.emoji-0032-20E3 { background-position: -40px 0; } .emoji-0033-20E3 { background-position: -40px -20px; } -.emoji-0034-20E3 { background-position: 0px -40px; } +.emoji-0034-20E3 { background-position: 0 -40px; } .emoji-0035-20E3 { background-position: -20px -40px; } .emoji-0036-20E3 { background-position: -40px -40px; } -.emoji-0037-20E3 { background-position: -60px 0px; } +.emoji-0037-20E3 { background-position: -60px 0; } .emoji-0038-20E3 { background-position: -60px -20px; } .emoji-0039-20E3 { background-position: -60px -40px; } -.emoji-00A9 { background-position: 0px -60px; } +.emoji-00A9 { background-position: 0 -60px; } .emoji-00AE { background-position: -20px -60px; } .emoji-1F004 { background-position: -40px -60px; } .emoji-1F0CF { background-position: -60px -60px; } -.emoji-1F170 { background-position: -80px 0px; } +.emoji-1F170 { background-position: -80px 0; } .emoji-1F171 { background-position: -80px -20px; } .emoji-1F17E { background-position: -80px -40px; } .emoji-1F17F { background-position: -80px -60px; } -.emoji-1F18E { background-position: 0px -80px; } +.emoji-1F18E { background-position: 0 -80px; } .emoji-1F191 { background-position: -20px -80px; } .emoji-1F192 { background-position: -40px -80px; } .emoji-1F193 { background-position: -60px -80px; } .emoji-1F194 { background-position: -80px -80px; } -.emoji-1F195 { background-position: -100px 0px; } +.emoji-1F195 { background-position: -100px 0; } .emoji-1F196 { background-position: -100px -20px; } .emoji-1F197 { background-position: -100px -40px; } .emoji-1F198 { background-position: -100px -60px; } .emoji-1F199 { background-position: -100px -80px; } -.emoji-1F19A { background-position: 0px -100px; } +.emoji-1F19A { background-position: 0 -100px; } .emoji-1F1E6-1F1E8 { background-position: -20px -100px; } .emoji-1F1E6-1F1E9 { background-position: -40px -100px; } .emoji-1F1E6-1F1EA { background-position: -60px -100px; } .emoji-1F1E6-1F1EB { background-position: -80px -100px; } .emoji-1F1E6-1F1EC { background-position: -100px -100px; } -.emoji-1F1E6-1F1EE { background-position: -120px 0px; } +.emoji-1F1E6-1F1EE { background-position: -120px 0; } .emoji-1F1E6-1F1F1 { background-position: -120px -20px; } .emoji-1F1E6-1F1F2 { background-position: -120px -40px; } .emoji-1F1E6-1F1F4 { background-position: -120px -60px; } .emoji-1F1E6-1F1F6 { background-position: -120px -80px; } .emoji-1F1E6-1F1F7 { background-position: -120px -100px; } -.emoji-1F1E6-1F1F8 { background-position: 0px -120px; } +.emoji-1F1E6-1F1F8 { background-position: 0 -120px; } .emoji-1F1E6-1F1F9 { background-position: -20px -120px; } .emoji-1F1E6-1F1FA { background-position: -40px -120px; } .emoji-1F1E6-1F1FC { background-position: -60px -120px; } .emoji-1F1E6-1F1FD { background-position: -80px -120px; } .emoji-1F1E6-1F1FF { background-position: -100px -120px; } .emoji-1F1E7-1F1E6 { background-position: -120px -120px; } -.emoji-1F1E7-1F1E7 { background-position: -140px 0px; } +.emoji-1F1E7-1F1E7 { background-position: -140px 0; } .emoji-1F1E7-1F1E9 { background-position: -140px -20px; } .emoji-1F1E7-1F1EA { background-position: -140px -40px; } .emoji-1F1E7-1F1EB { background-position: -140px -60px; } .emoji-1F1E7-1F1EC { background-position: -140px -80px; } .emoji-1F1E7-1F1ED { background-position: -140px -100px; } .emoji-1F1E7-1F1EE { background-position: -140px -120px; } -.emoji-1F1E7-1F1EF { background-position: 0px -140px; } +.emoji-1F1E7-1F1EF { background-position: 0 -140px; } .emoji-1F1E7-1F1F1 { background-position: -20px -140px; } .emoji-1F1E7-1F1F2 { background-position: -40px -140px; } .emoji-1F1E7-1F1F3 { background-position: -60px -140px; } @@ -62,7 +62,7 @@ .emoji-1F1E7-1F1F6 { background-position: -100px -140px; } .emoji-1F1E7-1F1F7 { background-position: -120px -140px; } .emoji-1F1E7-1F1F8 { background-position: -140px -140px; } -.emoji-1F1E7-1F1F9 { background-position: -160px 0px; } +.emoji-1F1E7-1F1F9 { background-position: -160px 0; } .emoji-1F1E7-1F1FB { background-position: -160px -20px; } .emoji-1F1E7-1F1FC { background-position: -160px -40px; } .emoji-1F1E7-1F1FE { background-position: -160px -60px; } @@ -70,7 +70,7 @@ .emoji-1F1E8-1F1E6 { background-position: -160px -100px; } .emoji-1F1E8-1F1E8 { background-position: -160px -120px; } .emoji-1F1E8-1F1E9 { background-position: -160px -140px; } -.emoji-1F1E8-1F1EB { background-position: 0px -160px; } +.emoji-1F1E8-1F1EB { background-position: 0 -160px; } .emoji-1F1E8-1F1EC { background-position: -20px -160px; } .emoji-1F1E8-1F1ED { background-position: -40px -160px; } .emoji-1F1E8-1F1EE { background-position: -60px -160px; } @@ -79,7 +79,7 @@ .emoji-1F1E8-1F1F2 { background-position: -120px -160px; } .emoji-1F1E8-1F1F3 { background-position: -140px -160px; } .emoji-1F1E8-1F1F4 { background-position: -160px -160px; } -.emoji-1F1E8-1F1F5 { background-position: -180px 0px; } +.emoji-1F1E8-1F1F5 { background-position: -180px 0; } .emoji-1F1E8-1F1F7 { background-position: -180px -20px; } .emoji-1F1E8-1F1FA { background-position: -180px -40px; } .emoji-1F1E8-1F1FB { background-position: -180px -60px; } @@ -88,7 +88,7 @@ .emoji-1F1E8-1F1FE { background-position: -180px -120px; } .emoji-1F1E8-1F1FF { background-position: -180px -140px; } .emoji-1F1E9-1F1EA { background-position: -180px -160px; } -.emoji-1F1E9-1F1EC { background-position: 0px -180px; } +.emoji-1F1E9-1F1EC { background-position: 0 -180px; } .emoji-1F1E9-1F1EF { background-position: -20px -180px; } .emoji-1F1E9-1F1F0 { background-position: -40px -180px; } .emoji-1F1E9-1F1F2 { background-position: -60px -180px; } @@ -98,7 +98,7 @@ .emoji-1F1EA-1F1E8 { background-position: -140px -180px; } .emoji-1F1EA-1F1EA { background-position: -160px -180px; } .emoji-1F1EA-1F1EC { background-position: -180px -180px; } -.emoji-1F1EA-1F1ED { background-position: -200px 0px; } +.emoji-1F1EA-1F1ED { background-position: -200px 0; } .emoji-1F1EA-1F1F7 { background-position: -200px -20px; } .emoji-1F1EA-1F1F8 { background-position: -200px -40px; } .emoji-1F1EA-1F1F9 { background-position: -200px -60px; } @@ -108,7 +108,7 @@ .emoji-1F1EB-1F1F0 { background-position: -200px -140px; } .emoji-1F1EB-1F1F2 { background-position: -200px -160px; } .emoji-1F1EB-1F1F4 { background-position: -200px -180px; } -.emoji-1F1EB-1F1F7 { background-position: 0px -200px; } +.emoji-1F1EB-1F1F7 { background-position: 0 -200px; } .emoji-1F1EC-1F1E6 { background-position: -20px -200px; } .emoji-1F1EC-1F1E7 { background-position: -40px -200px; } .emoji-1F1EC-1F1E9 { background-position: -60px -200px; } @@ -119,7 +119,7 @@ .emoji-1F1EC-1F1EE { background-position: -160px -200px; } .emoji-1F1EC-1F1F1 { background-position: -180px -200px; } .emoji-1F1EC-1F1F2 { background-position: -200px -200px; } -.emoji-1F1EC-1F1F3 { background-position: -220px 0px; } +.emoji-1F1EC-1F1F3 { background-position: -220px 0; } .emoji-1F1EC-1F1F5 { background-position: -220px -20px; } .emoji-1F1EC-1F1F6 { background-position: -220px -40px; } .emoji-1F1EC-1F1F7 { background-position: -220px -60px; } @@ -130,7 +130,7 @@ .emoji-1F1EC-1F1FE { background-position: -220px -160px; } .emoji-1F1ED-1F1F0 { background-position: -220px -180px; } .emoji-1F1ED-1F1F2 { background-position: -220px -200px; } -.emoji-1F1ED-1F1F3 { background-position: 0px -220px; } +.emoji-1F1ED-1F1F3 { background-position: 0 -220px; } .emoji-1F1ED-1F1F7 { background-position: -20px -220px; } .emoji-1F1ED-1F1F9 { background-position: -40px -220px; } .emoji-1F1ED-1F1FA { background-position: -60px -220px; } @@ -142,7 +142,7 @@ .emoji-1F1EE-1F1F3 { background-position: -180px -220px; } .emoji-1F1EE-1F1F4 { background-position: -200px -220px; } .emoji-1F1EE-1F1F6 { background-position: -220px -220px; } -.emoji-1F1EE-1F1F7 { background-position: -240px 0px; } +.emoji-1F1EE-1F1F7 { background-position: -240px 0; } .emoji-1F1EE-1F1F8 { background-position: -240px -20px; } .emoji-1F1EE-1F1F9 { background-position: -240px -40px; } .emoji-1F1EF-1F1EA { background-position: -240px -60px; } @@ -154,7 +154,7 @@ .emoji-1F1F0-1F1ED { background-position: -240px -180px; } .emoji-1F1F0-1F1EE { background-position: -240px -200px; } .emoji-1F1F0-1F1F2 { background-position: -240px -220px; } -.emoji-1F1F0-1F1F3 { background-position: 0px -240px; } +.emoji-1F1F0-1F1F3 { background-position: 0 -240px; } .emoji-1F1F0-1F1F5 { background-position: -20px -240px; } .emoji-1F1F0-1F1F7 { background-position: -40px -240px; } .emoji-1F1F0-1F1FC { background-position: -60px -240px; } @@ -167,7 +167,7 @@ .emoji-1F1F1-1F1F0 { background-position: -200px -240px; } .emoji-1F1F1-1F1F7 { background-position: -220px -240px; } .emoji-1F1F1-1F1F8 { background-position: -240px -240px; } -.emoji-1F1F1-1F1F9 { background-position: -260px 0px; } +.emoji-1F1F1-1F1F9 { background-position: -260px 0; } .emoji-1F1F1-1F1FA { background-position: -260px -20px; } .emoji-1F1F1-1F1FB { background-position: -260px -40px; } .emoji-1F1F1-1F1FE { background-position: -260px -60px; } @@ -180,7 +180,7 @@ .emoji-1F1F2-1F1ED { background-position: -260px -200px; } .emoji-1F1F2-1F1F0 { background-position: -260px -220px; } .emoji-1F1F2-1F1F1 { background-position: -260px -240px; } -.emoji-1F1F2-1F1F2 { background-position: 0px -260px; } +.emoji-1F1F2-1F1F2 { background-position: 0 -260px; } .emoji-1F1F2-1F1F3 { background-position: -20px -260px; } .emoji-1F1F2-1F1F4 { background-position: -40px -260px; } .emoji-1F1F2-1F1F5 { background-position: -60px -260px; } @@ -194,7 +194,7 @@ .emoji-1F1F2-1F1FD { background-position: -220px -260px; } .emoji-1F1F2-1F1FE { background-position: -240px -260px; } .emoji-1F1F2-1F1FF { background-position: -260px -260px; } -.emoji-1F1F3-1F1E6 { background-position: -280px 0px; } +.emoji-1F1F3-1F1E6 { background-position: -280px 0; } .emoji-1F1F3-1F1E8 { background-position: -280px -20px; } .emoji-1F1F3-1F1EA { background-position: -280px -40px; } .emoji-1F1F3-1F1EB { background-position: -280px -60px; } @@ -208,7 +208,7 @@ .emoji-1F1F3-1F1FF { background-position: -280px -220px; } .emoji-1F1F4-1F1F2 { background-position: -280px -240px; } .emoji-1F1F5-1F1E6 { background-position: -280px -260px; } -.emoji-1F1F5-1F1EA { background-position: 0px -280px; } +.emoji-1F1F5-1F1EA { background-position: 0 -280px; } .emoji-1F1F5-1F1EB { background-position: -20px -280px; } .emoji-1F1F5-1F1EC { background-position: -40px -280px; } .emoji-1F1F5-1F1ED { background-position: -60px -280px; } @@ -223,7 +223,7 @@ .emoji-1F1F5-1F1FE { background-position: -240px -280px; } .emoji-1F1F6-1F1E6 { background-position: -260px -280px; } .emoji-1F1F7-1F1EA { background-position: -280px -280px; } -.emoji-1F1F7-1F1F4 { background-position: -300px 0px; } +.emoji-1F1F7-1F1F4 { background-position: -300px 0; } .emoji-1F1F7-1F1F8 { background-position: -300px -20px; } .emoji-1F1F7-1F1FA { background-position: -300px -40px; } .emoji-1F1F7-1F1FC { background-position: -300px -60px; } @@ -238,7 +238,7 @@ .emoji-1F1F8-1F1EF { background-position: -300px -240px; } .emoji-1F1F8-1F1F0 { background-position: -300px -260px; } .emoji-1F1F8-1F1F1 { background-position: -300px -280px; } -.emoji-1F1F8-1F1F2 { background-position: 0px -300px; } +.emoji-1F1F8-1F1F2 { background-position: 0 -300px; } .emoji-1F1F8-1F1F3 { background-position: -20px -300px; } .emoji-1F1F8-1F1F4 { background-position: -40px -300px; } .emoji-1F1F8-1F1F7 { background-position: -60px -300px; } @@ -254,7 +254,7 @@ .emoji-1F1F9-1F1EB { background-position: -260px -300px; } .emoji-1F1F9-1F1EC { background-position: -280px -300px; } .emoji-1F1F9-1F1ED { background-position: -300px -300px; } -.emoji-1F1F9-1F1EF { background-position: -320px 0px; } +.emoji-1F1F9-1F1EF { background-position: -320px 0; } .emoji-1F1F9-1F1F0 { background-position: -320px -20px; } .emoji-1F1F9-1F1F1 { background-position: -320px -40px; } .emoji-1F1F9-1F1F2 { background-position: -320px -60px; } @@ -270,7 +270,7 @@ .emoji-1F1FA-1F1F2 { background-position: -320px -260px; } .emoji-1F1FA-1F1F8 { background-position: -320px -280px; } .emoji-1F1FA-1F1FE { background-position: -320px -300px; } -.emoji-1F1FA-1F1FF { background-position: 0px -320px; } +.emoji-1F1FA-1F1FF { background-position: 0 -320px; } .emoji-1F1FB-1F1E6 { background-position: -20px -320px; } .emoji-1F1FB-1F1E8 { background-position: -40px -320px; } .emoji-1F1FB-1F1EA { background-position: -60px -320px; } @@ -287,7 +287,7 @@ .emoji-1F1FF-1F1F2 { background-position: -280px -320px; } .emoji-1F1FF-1F1FC { background-position: -300px -320px; } .emoji-1F201 { background-position: -320px -320px; } -.emoji-1F202 { background-position: -340px 0px; } +.emoji-1F202 { background-position: -340px 0; } .emoji-1F21A { background-position: -340px -20px; } .emoji-1F22F { background-position: -340px -40px; } .emoji-1F232 { background-position: -340px -60px; } @@ -304,7 +304,7 @@ .emoji-1F300 { background-position: -340px -280px; } .emoji-1F301 { background-position: -340px -300px; } .emoji-1F302 { background-position: -340px -320px; } -.emoji-1F303 { background-position: 0px -340px; } +.emoji-1F303 { background-position: 0 -340px; } .emoji-1F304 { background-position: -20px -340px; } .emoji-1F305 { background-position: -40px -340px; } .emoji-1F306 { background-position: -60px -340px; } @@ -322,7 +322,7 @@ .emoji-1F312 { background-position: -300px -340px; } .emoji-1F313 { background-position: -320px -340px; } .emoji-1F314 { background-position: -340px -340px; } -.emoji-1F315 { background-position: -360px 0px; } +.emoji-1F315 { background-position: -360px 0; } .emoji-1F316 { background-position: -360px -20px; } .emoji-1F317 { background-position: -360px -40px; } .emoji-1F318 { background-position: -360px -60px; } @@ -340,7 +340,7 @@ .emoji-1F326 { background-position: -360px -300px; } .emoji-1F327 { background-position: -360px -320px; } .emoji-1F328 { background-position: -360px -340px; } -.emoji-1F329 { background-position: 0px -360px; } +.emoji-1F329 { background-position: 0 -360px; } .emoji-1F32A { background-position: -20px -360px; } .emoji-1F32B { background-position: -40px -360px; } .emoji-1F32C { background-position: -60px -360px; } @@ -359,7 +359,7 @@ .emoji-1F339 { background-position: -320px -360px; } .emoji-1F33A { background-position: -340px -360px; } .emoji-1F33B { background-position: -360px -360px; } -.emoji-1F33C { background-position: -380px 0px; } +.emoji-1F33C { background-position: -380px 0; } .emoji-1F33D { background-position: -380px -20px; } .emoji-1F33E { background-position: -380px -40px; } .emoji-1F33F { background-position: -380px -60px; } @@ -378,7 +378,7 @@ .emoji-1F34C { background-position: -380px -320px; } .emoji-1F34D { background-position: -380px -340px; } .emoji-1F34E { background-position: -380px -360px; } -.emoji-1F34F { background-position: 0px -380px; } +.emoji-1F34F { background-position: 0 -380px; } .emoji-1F350 { background-position: -20px -380px; } .emoji-1F351 { background-position: -40px -380px; } .emoji-1F352 { background-position: -60px -380px; } @@ -398,7 +398,7 @@ .emoji-1F360 { background-position: -340px -380px; } .emoji-1F361 { background-position: -360px -380px; } .emoji-1F362 { background-position: -380px -380px; } -.emoji-1F363 { background-position: -400px 0px; } +.emoji-1F363 { background-position: -400px 0; } .emoji-1F364 { background-position: -400px -20px; } .emoji-1F365 { background-position: -400px -40px; } .emoji-1F366 { background-position: -400px -60px; } @@ -418,7 +418,7 @@ .emoji-1F374 { background-position: -400px -340px; } .emoji-1F375 { background-position: -400px -360px; } .emoji-1F376 { background-position: -400px -380px; } -.emoji-1F377 { background-position: 0px -400px; } +.emoji-1F377 { background-position: 0 -400px; } .emoji-1F378 { background-position: -20px -400px; } .emoji-1F379 { background-position: -40px -400px; } .emoji-1F37A { background-position: -60px -400px; } @@ -439,7 +439,7 @@ .emoji-1F385-1F3FE { background-position: -360px -400px; } .emoji-1F385-1F3FF { background-position: -380px -400px; } .emoji-1F386 { background-position: -400px -400px; } -.emoji-1F387 { background-position: -420px 0px; } +.emoji-1F387 { background-position: -420px 0; } .emoji-1F388 { background-position: -420px -20px; } .emoji-1F389 { background-position: -420px -40px; } .emoji-1F38A { background-position: -420px -60px; } @@ -460,7 +460,7 @@ .emoji-1F399 { background-position: -420px -360px; } .emoji-1F39A { background-position: -420px -380px; } .emoji-1F39B { background-position: -420px -400px; } -.emoji-1F39C { background-position: 0px -420px; } +.emoji-1F39C { background-position: 0 -420px; } .emoji-1F39D { background-position: -20px -420px; } .emoji-1F39E { background-position: -40px -420px; } .emoji-1F39F { background-position: -60px -420px; } @@ -482,7 +482,7 @@ .emoji-1F3AF { background-position: -380px -420px; } .emoji-1F3B0 { background-position: -400px -420px; } .emoji-1F3B1 { background-position: -420px -420px; } -.emoji-1F3B2 { background-position: -440px 0px; } +.emoji-1F3B2 { background-position: -440px 0; } .emoji-1F3B3 { background-position: -440px -20px; } .emoji-1F3B4 { background-position: -440px -40px; } .emoji-1F3B5 { background-position: -440px -60px; } @@ -504,7 +504,7 @@ .emoji-1F3C3-1F3FC { background-position: -440px -380px; } .emoji-1F3C3-1F3FD { background-position: -440px -400px; } .emoji-1F3C3-1F3FE { background-position: -440px -420px; } -.emoji-1F3C3-1F3FF { background-position: 0px -440px; } +.emoji-1F3C3-1F3FF { background-position: 0 -440px; } .emoji-1F3C4 { background-position: -20px -440px; } .emoji-1F3C4-1F3FB { background-position: -40px -440px; } .emoji-1F3C4-1F3FC { background-position: -60px -440px; } @@ -527,7 +527,7 @@ .emoji-1F3CA-1F3FD { background-position: -400px -440px; } .emoji-1F3CA-1F3FE { background-position: -420px -440px; } .emoji-1F3CA-1F3FF { background-position: -440px -440px; } -.emoji-1F3CB { background-position: -460px 0px; } +.emoji-1F3CB { background-position: -460px 0; } .emoji-1F3CB-1F3FB { background-position: -460px -20px; } .emoji-1F3CB-1F3FC { background-position: -460px -40px; } .emoji-1F3CB-1F3FD { background-position: -460px -60px; } @@ -550,7 +550,7 @@ .emoji-1F3DA { background-position: -460px -400px; } .emoji-1F3DB { background-position: -460px -420px; } .emoji-1F3DC { background-position: -460px -440px; } -.emoji-1F3DD { background-position: 0px -460px; } +.emoji-1F3DD { background-position: 0 -460px; } .emoji-1F3DE { background-position: -20px -460px; } .emoji-1F3DF { background-position: -40px -460px; } .emoji-1F3E0 { background-position: -60px -460px; } @@ -574,7 +574,7 @@ .emoji-1F3F2 { background-position: -420px -460px; } .emoji-1F3F3 { background-position: -440px -460px; } .emoji-1F3F4 { background-position: -460px -460px; } -.emoji-1F3F5 { background-position: -480px 0px; } +.emoji-1F3F5 { background-position: -480px 0; } .emoji-1F3F6 { background-position: -480px -20px; } .emoji-1F3F7 { background-position: -480px -40px; } .emoji-1F3F8 { background-position: -480px -60px; } @@ -598,7 +598,7 @@ .emoji-1F40A { background-position: -480px -420px; } .emoji-1F40B { background-position: -480px -440px; } .emoji-1F40C { background-position: -480px -460px; } -.emoji-1F40D { background-position: 0px -480px; } +.emoji-1F40D { background-position: 0 -480px; } .emoji-1F40E { background-position: -20px -480px; } .emoji-1F40F { background-position: -40px -480px; } .emoji-1F410 { background-position: -60px -480px; } @@ -623,7 +623,7 @@ .emoji-1F423 { background-position: -440px -480px; } .emoji-1F424 { background-position: -460px -480px; } .emoji-1F425 { background-position: -480px -480px; } -.emoji-1F426 { background-position: -500px 0px; } +.emoji-1F426 { background-position: -500px 0; } .emoji-1F427 { background-position: -500px -20px; } .emoji-1F428 { background-position: -500px -40px; } .emoji-1F429 { background-position: -500px -60px; } @@ -648,7 +648,7 @@ .emoji-1F43C { background-position: -500px -440px; } .emoji-1F43D { background-position: -500px -460px; } .emoji-1F43E { background-position: -500px -480px; } -.emoji-1F43F { background-position: 0px -500px; } +.emoji-1F43F { background-position: 0 -500px; } .emoji-1F440 { background-position: -20px -500px; } .emoji-1F441 { background-position: -40px -500px; } .emoji-1F441-1F5E8 { background-position: -60px -500px; } @@ -674,7 +674,7 @@ .emoji-1F446-1F3FF { background-position: -460px -500px; } .emoji-1F447 { background-position: -480px -500px; } .emoji-1F447-1F3FB { background-position: -500px -500px; } -.emoji-1F447-1F3FC { background-position: -520px 0px; } +.emoji-1F447-1F3FC { background-position: -520px 0; } .emoji-1F447-1F3FD { background-position: -520px -20px; } .emoji-1F447-1F3FE { background-position: -520px -40px; } .emoji-1F447-1F3FF { background-position: -520px -60px; } @@ -700,7 +700,7 @@ .emoji-1F44B-1F3FB { background-position: -520px -460px; } .emoji-1F44B-1F3FC { background-position: -520px -480px; } .emoji-1F44B-1F3FD { background-position: -520px -500px; } -.emoji-1F44B-1F3FE { background-position: 0px -520px; } +.emoji-1F44B-1F3FE { background-position: 0 -520px; } .emoji-1F44B-1F3FF { background-position: -20px -520px; } .emoji-1F44C { background-position: -40px -520px; } .emoji-1F44C-1F3FB { background-position: -60px -520px; } @@ -727,7 +727,7 @@ .emoji-1F44F-1F3FE { background-position: -480px -520px; } .emoji-1F44F-1F3FF { background-position: -500px -520px; } .emoji-1F450 { background-position: -520px -520px; } -.emoji-1F450-1F3FB { background-position: -540px 0px; } +.emoji-1F450-1F3FB { background-position: -540px 0; } .emoji-1F450-1F3FC { background-position: -540px -20px; } .emoji-1F450-1F3FD { background-position: -540px -40px; } .emoji-1F450-1F3FE { background-position: -540px -60px; } @@ -754,7 +754,7 @@ .emoji-1F464 { background-position: -540px -480px; } .emoji-1F465 { background-position: -540px -500px; } .emoji-1F466 { background-position: -540px -520px; } -.emoji-1F466-1F3FB { background-position: 0px -540px; } +.emoji-1F466-1F3FB { background-position: 0 -540px; } .emoji-1F466-1F3FC { background-position: -20px -540px; } .emoji-1F466-1F3FD { background-position: -40px -540px; } .emoji-1F466-1F3FE { background-position: -60px -540px; } @@ -782,7 +782,7 @@ .emoji-1F468-1F469-1F467-1F467 { background-position: -500px -540px; } .emoji-1F468-2764-1F468 { background-position: -520px -540px; } .emoji-1F468-2764-1F48B-1F468 { background-position: -540px -540px; } -.emoji-1F469 { background-position: -560px 0px; } +.emoji-1F469 { background-position: -560px 0; } .emoji-1F469-1F3FB { background-position: -560px -20px; } .emoji-1F469-1F3FC { background-position: -560px -40px; } .emoji-1F469-1F3FD { background-position: -560px -60px; } @@ -810,7 +810,7 @@ .emoji-1F470-1F3FB { background-position: -560px -500px; } .emoji-1F470-1F3FC { background-position: -560px -520px; } .emoji-1F470-1F3FD { background-position: -560px -540px; } -.emoji-1F470-1F3FE { background-position: 0px -560px; } +.emoji-1F470-1F3FE { background-position: 0 -560px; } .emoji-1F470-1F3FF { background-position: -20px -560px; } .emoji-1F471 { background-position: -40px -560px; } .emoji-1F471-1F3FB { background-position: -60px -560px; } @@ -839,7 +839,7 @@ .emoji-1F475 { background-position: -520px -560px; } .emoji-1F475-1F3FB { background-position: -540px -560px; } .emoji-1F475-1F3FC { background-position: -560px -560px; } -.emoji-1F475-1F3FD { background-position: -580px 0px; } +.emoji-1F475-1F3FD { background-position: -580px 0; } .emoji-1F475-1F3FE { background-position: -580px -20px; } .emoji-1F475-1F3FF { background-position: -580px -40px; } .emoji-1F476 { background-position: -580px -60px; } @@ -868,7 +868,7 @@ .emoji-1F47C-1F3FC { background-position: -580px -520px; } .emoji-1F47C-1F3FD { background-position: -580px -540px; } .emoji-1F47C-1F3FE { background-position: -580px -560px; } -.emoji-1F47C-1F3FF { background-position: 0px -580px; } +.emoji-1F47C-1F3FF { background-position: 0 -580px; } .emoji-1F47D { background-position: -20px -580px; } .emoji-1F47E { background-position: -40px -580px; } .emoji-1F47F { background-position: -60px -580px; } @@ -898,7 +898,7 @@ .emoji-1F485-1F3FD { background-position: -540px -580px; } .emoji-1F485-1F3FE { background-position: -560px -580px; } .emoji-1F485-1F3FF { background-position: -580px -580px; } -.emoji-1F486 { background-position: -600px 0px; } +.emoji-1F486 { background-position: -600px 0; } .emoji-1F486-1F3FB { background-position: -600px -20px; } .emoji-1F486-1F3FC { background-position: -600px -40px; } .emoji-1F486-1F3FD { background-position: -600px -60px; } @@ -928,7 +928,7 @@ .emoji-1F497 { background-position: -600px -540px; } .emoji-1F498 { background-position: -600px -560px; } .emoji-1F499 { background-position: -600px -580px; } -.emoji-1F49A { background-position: 0px -600px; } +.emoji-1F49A { background-position: 0 -600px; } .emoji-1F49B { background-position: -20px -600px; } .emoji-1F49C { background-position: -40px -600px; } .emoji-1F49D { background-position: -60px -600px; } @@ -959,7 +959,7 @@ .emoji-1F4B1 { background-position: -560px -600px; } .emoji-1F4B2 { background-position: -580px -600px; } .emoji-1F4B3 { background-position: -600px -600px; } -.emoji-1F4B4 { background-position: -620px 0px; } +.emoji-1F4B4 { background-position: -620px 0; } .emoji-1F4B5 { background-position: -620px -20px; } .emoji-1F4B6 { background-position: -620px -40px; } .emoji-1F4B7 { background-position: -620px -60px; } @@ -990,7 +990,7 @@ .emoji-1F4D0 { background-position: -620px -560px; } .emoji-1F4D1 { background-position: -620px -580px; } .emoji-1F4D2 { background-position: -620px -600px; } -.emoji-1F4D3 { background-position: 0px -620px; } +.emoji-1F4D3 { background-position: 0 -620px; } .emoji-1F4D4 { background-position: -20px -620px; } .emoji-1F4D5 { background-position: -40px -620px; } .emoji-1F4D6 { background-position: -60px -620px; } @@ -1022,7 +1022,7 @@ .emoji-1F4F0 { background-position: -580px -620px; } .emoji-1F4F1 { background-position: -600px -620px; } .emoji-1F4F2 { background-position: -620px -620px; } -.emoji-1F4F3 { background-position: -640px 0px; } +.emoji-1F4F3 { background-position: -640px 0; } .emoji-1F4F4 { background-position: -640px -20px; } .emoji-1F4F5 { background-position: -640px -40px; } .emoji-1F4F6 { background-position: -640px -60px; } @@ -1054,7 +1054,7 @@ .emoji-1F510 { background-position: -640px -580px; } .emoji-1F511 { background-position: -640px -600px; } .emoji-1F512 { background-position: -640px -620px; } -.emoji-1F513 { background-position: 0px -640px; } +.emoji-1F513 { background-position: 0 -640px; } .emoji-1F514 { background-position: -20px -640px; } .emoji-1F515 { background-position: -40px -640px; } .emoji-1F516 { background-position: -60px -640px; } @@ -1087,7 +1087,7 @@ .emoji-1F531 { background-position: -600px -640px; } .emoji-1F532 { background-position: -620px -640px; } .emoji-1F533 { background-position: -640px -640px; } -.emoji-1F534 { background-position: -660px 0px; } +.emoji-1F534 { background-position: -660px 0; } .emoji-1F535 { background-position: -660px -20px; } .emoji-1F536 { background-position: -660px -40px; } .emoji-1F537 { background-position: -660px -60px; } @@ -1120,7 +1120,7 @@ .emoji-1F55B { background-position: -660px -600px; } .emoji-1F55C { background-position: -660px -620px; } .emoji-1F55D { background-position: -660px -640px; } -.emoji-1F55E { background-position: 0px -660px; } +.emoji-1F55E { background-position: 0 -660px; } .emoji-1F55F { background-position: -20px -660px; } .emoji-1F560 { background-position: -40px -660px; } .emoji-1F561 { background-position: -60px -660px; } @@ -1154,7 +1154,7 @@ .emoji-1F578 { background-position: -620px -660px; } .emoji-1F579 { background-position: -640px -660px; } .emoji-1F57B { background-position: -660px -660px; } -.emoji-1F57E { background-position: -680px 0px; } +.emoji-1F57E { background-position: -680px 0; } .emoji-1F57F { background-position: -680px -20px; } .emoji-1F581 { background-position: -680px -40px; } .emoji-1F582 { background-position: -680px -60px; } @@ -1188,7 +1188,7 @@ .emoji-1F595-1F3FF { background-position: -680px -620px; } .emoji-1F596 { background-position: -680px -640px; } .emoji-1F596-1F3FB { background-position: -680px -660px; } -.emoji-1F596-1F3FC { background-position: 0px -680px; } +.emoji-1F596-1F3FC { background-position: 0 -680px; } .emoji-1F596-1F3FD { background-position: -20px -680px; } .emoji-1F596-1F3FE { background-position: -40px -680px; } .emoji-1F596-1F3FF { background-position: -60px -680px; } @@ -1223,7 +1223,7 @@ .emoji-1F5C4 { background-position: -640px -680px; } .emoji-1F5C6 { background-position: -660px -680px; } .emoji-1F5C7 { background-position: -680px -680px; } -.emoji-1F5C9 { background-position: -700px 0px; } +.emoji-1F5C9 { background-position: -700px 0; } .emoji-1F5CA { background-position: -700px -20px; } .emoji-1F5CE { background-position: -700px -40px; } .emoji-1F5CF { background-position: -700px -60px; } @@ -1258,7 +1258,7 @@ .emoji-1F5F8 { background-position: -700px -640px; } .emoji-1F5F9 { background-position: -700px -660px; } .emoji-1F5FA { background-position: -700px -680px; } -.emoji-1F5FB { background-position: 0px -700px; } +.emoji-1F5FB { background-position: 0 -700px; } .emoji-1F5FC { background-position: -20px -700px; } .emoji-1F5FD { background-position: -40px -700px; } .emoji-1F5FE { background-position: -60px -700px; } @@ -1294,7 +1294,7 @@ .emoji-1F61C { background-position: -660px -700px; } .emoji-1F61D { background-position: -680px -700px; } .emoji-1F61E { background-position: -700px -700px; } -.emoji-1F61F { background-position: -720px 0px; } +.emoji-1F61F { background-position: -720px 0; } .emoji-1F620 { background-position: -720px -20px; } .emoji-1F621 { background-position: -720px -40px; } .emoji-1F622 { background-position: -720px -60px; } @@ -1330,7 +1330,7 @@ .emoji-1F640 { background-position: -720px -660px; } .emoji-1F641 { background-position: -720px -680px; } .emoji-1F642 { background-position: -720px -700px; } -.emoji-1F643 { background-position: 0px -720px; } +.emoji-1F643 { background-position: 0 -720px; } .emoji-1F644 { background-position: -20px -720px; } .emoji-1F645 { background-position: -40px -720px; } .emoji-1F645-1F3FB { background-position: -60px -720px; } @@ -1367,7 +1367,7 @@ .emoji-1F64C-1F3FF { background-position: -680px -720px; } .emoji-1F64D { background-position: -700px -720px; } .emoji-1F64D-1F3FB { background-position: -720px -720px; } -.emoji-1F64D-1F3FC { background-position: -740px 0px; } +.emoji-1F64D-1F3FC { background-position: -740px 0; } .emoji-1F64D-1F3FD { background-position: -740px -20px; } .emoji-1F64D-1F3FE { background-position: -740px -40px; } .emoji-1F64D-1F3FF { background-position: -740px -60px; } @@ -1404,7 +1404,7 @@ .emoji-1F692 { background-position: -740px -680px; } .emoji-1F693 { background-position: -740px -700px; } .emoji-1F694 { background-position: -740px -720px; } -.emoji-1F695 { background-position: 0px -740px; } +.emoji-1F695 { background-position: 0 -740px; } .emoji-1F696 { background-position: -20px -740px; } .emoji-1F697 { background-position: -40px -740px; } .emoji-1F698 { background-position: -60px -740px; } @@ -1442,7 +1442,7 @@ .emoji-1F6B3 { background-position: -700px -740px; } .emoji-1F6B4 { background-position: -720px -740px; } .emoji-1F6B4-1F3FB { background-position: -740px -740px; } -.emoji-1F6B4-1F3FC { background-position: -760px 0px; } +.emoji-1F6B4-1F3FC { background-position: -760px 0; } .emoji-1F6B4-1F3FD { background-position: -760px -20px; } .emoji-1F6B4-1F3FE { background-position: -760px -40px; } .emoji-1F6B4-1F3FF { background-position: -760px -60px; } @@ -1480,7 +1480,7 @@ .emoji-1F6C5 { background-position: -760px -700px; } .emoji-1F6C6 { background-position: -760px -720px; } .emoji-1F6C7 { background-position: -760px -740px; } -.emoji-1F6C8 { background-position: 0px -760px; } +.emoji-1F6C8 { background-position: 0 -760px; } .emoji-1F6C9 { background-position: -20px -760px; } .emoji-1F6CA { background-position: -40px -760px; } .emoji-1F6CB { background-position: -60px -760px; } @@ -1519,7 +1519,7 @@ .emoji-1F918-1F3FC { background-position: -720px -760px; } .emoji-1F918-1F3FD { background-position: -740px -760px; } .emoji-1F918-1F3FE { background-position: -760px -760px; } -.emoji-1F918-1F3FF { background-position: -780px 0px; } +.emoji-1F918-1F3FF { background-position: -780px 0; } .emoji-1F980 { background-position: -780px -20px; } .emoji-1F981 { background-position: -780px -40px; } .emoji-1F982 { background-position: -780px -60px; } @@ -1558,7 +1558,7 @@ .emoji-24C2 { background-position: -780px -720px; } .emoji-25AA { background-position: -780px -740px; } .emoji-25AB { background-position: -780px -760px; } -.emoji-25B6 { background-position: 0px -780px; } +.emoji-25B6 { background-position: 0 -780px; } .emoji-25C0 { background-position: -20px -780px; } .emoji-25FB { background-position: -40px -780px; } .emoji-25FC { background-position: -60px -780px; } @@ -1598,7 +1598,7 @@ .emoji-264D { background-position: -740px -780px; } .emoji-264E { background-position: -760px -780px; } .emoji-264F { background-position: -780px -780px; } -.emoji-2650 { background-position: -800px 0px; } +.emoji-2650 { background-position: -800px 0; } .emoji-2651 { background-position: -800px -20px; } .emoji-2652 { background-position: -800px -40px; } .emoji-2653 { background-position: -800px -60px; } @@ -1638,7 +1638,7 @@ .emoji-26F0 { background-position: -800px -740px; } .emoji-26F1 { background-position: -800px -760px; } .emoji-26F2 { background-position: -800px -780px; } -.emoji-26F3 { background-position: 0px -800px; } +.emoji-26F3 { background-position: 0 -800px; } .emoji-26F4 { background-position: -20px -800px; } .emoji-26F5 { background-position: -40px -800px; } .emoji-26F7 { background-position: -60px -800px; } @@ -1679,7 +1679,7 @@ .emoji-270D-1F3FD { background-position: -760px -800px; } .emoji-270D-1F3FE { background-position: -780px -800px; } .emoji-270D-1F3FF { background-position: -800px -800px; } -.emoji-270F { background-position: -820px 0px; } +.emoji-270F { background-position: -820px 0; } .emoji-2712 { background-position: -820px -20px; } .emoji-2714 { background-position: -820px -40px; } .emoji-2716 { background-position: -820px -60px; } diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 35df9a61c86..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 { @@ -63,7 +60,7 @@ .note-image-attach { margin-top: 4px; - margin-left: 0px; + margin-left: 0; max-width: 200px; float: none; } @@ -83,10 +80,10 @@ .event_icon { position: relative; float: right; - border: 1px solid #EEE; + border: 1px solid #eee; padding: 5px; @include border-radius(5px); - background: #F9F9F9; + background: #f9f9f9; margin-left: 10px; top: -6px; img { @@ -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; @@ -160,7 +157,7 @@ .event-body { margin: 0; - border-left: 2px solid #DDD; + border-left: 2px solid #ddd; padding-left: 10px; } diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss index c3b10d144e1..4e5c4ed84b6 100644 --- a/app/assets/stylesheets/pages/graph.scss +++ b/app/assets/stylesheets/pages/graph.scss @@ -6,11 +6,11 @@ font-size: 14px; padding: 5px; border-bottom: 1px solid $border-color; - background: #EEE; + background: #eee; } .network-graph { - background: #FFF; + background: #fff; height: 500px; overflow-y: scroll; overflow-x: hidden; diff --git a/app/assets/stylesheets/pages/import.scss b/app/assets/stylesheets/pages/import.scss index 3df4bb84bd2..6a99cd9cb94 100644 --- a/app/assets/stylesheets/pages/import.scss +++ b/app/assets/stylesheets/pages/import.scss @@ -1,6 +1,6 @@ i.icon-gitorious { display: inline-block; - background-position: 0px 0px; + background-position: 0 0; background-size: contain; background-repeat: no-repeat; } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 1310e6ad7c7..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 { @@ -222,13 +207,17 @@ width: 100%; text-align: center; padding-bottom: 10px; - color: #999999; + color: #999; span { display: block; margin-top: 0; } + .author { + display: none; + } + .btn-clipboard { border: none; @@ -237,10 +226,15 @@ } i { - color: #999999; + color: #999; } } } + + .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 1b686c58eaf..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; } @@ -68,18 +68,18 @@ form.edit-issue { .merge-request, .issue { &.today { - background: #EFE; - border-color: #CEC; + background: #efe; + border-color: #cec; } &.closed { - background: #F9F9F9; - border-color: #E5E5E5; + background: #f9f9f9; + border-color: #e5e5e5; } &.merged { - background: #F9F9F9; - border-color: #E5E5E5; + background: #f9f9f9; + border-color: #e5e5e5; } } @@ -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 61bec02f6c5..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 { @@ -39,7 +39,7 @@ .login-box{ background: #fafafa; border-radius: 10px; - box-shadow: 0 0px 2px #CCC; + box-shadow: 0 0 2px #ccc; padding: 15px; .login-heading h3 { @@ -74,7 +74,7 @@ &.top { @include border-radius(5px 5px 0 0); - margin-bottom: 0px; + margin-bottom: 0; } &.bottom { @@ -85,12 +85,12 @@ &.middle { border-top: 0; - margin-bottom:0px; + margin-bottom: 0; @include border-radius(0); } &:active, &:focus { - background-color: #FFF; + background-color: #fff; } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 2772623f4bd..cee5c47cfb2 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -113,7 +113,7 @@ } .mr-widget-footer { - border-top: 1px solid #EEE; + border-top: 1px solid #eee; } .ci-coverage { @@ -222,7 +222,7 @@ margin-bottom: 20px; span { - color: #B2B2B2; + color: #b2b2b2; a { color: $md-link-color; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 158c2a47862..61783ec46aa 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -156,7 +156,7 @@ .comment-hints { color: #999; - background: #FFF; + background: #fff; padding: 7px; margin-top: -7px; border: 1px solid $border-color; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index d5f9852ebed..d408853cc80 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -3,16 +3,16 @@ */ @-webkit-keyframes targe3-note { - from { background:#fffff0; } - 50% { background:#ffffd3; } - to { background:#fffff0; } + from { background: #fffff0; } + 50% { background: #ffffd3; } + to { background: #fffff0; } } ul.notes { display: block; list-style: none; - margin: 0px; - padding: 0px; + margin: 0; + padding: 0; .timeline-icon { float: left; @@ -30,7 +30,7 @@ ul.notes { font-size: 14px; padding-top: 10px; padding-bottom: 10px; - background: #FDFDFD; + background: #fdfdfd; .timeline-icon { .avatar { @@ -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; @@ -129,7 +136,7 @@ ul.notes { hr { // Darken 'whitesmoke' a bit to make it more visible in note bodies - border-color: darken(#F5F5F5, 8%); + border-color: darken(#f5f5f5, 8%); margin: 10px 0; } } @@ -166,7 +173,7 @@ ul.notes { vertical-align: middle; text-align: center; padding: 10px 0; - background: #FFF; + background: #fff; color: $text-color; } &.notes_line2 { @@ -232,7 +239,7 @@ ul.notes { .add-diff-note { margin-top: -4px; @include border-radius(40px); - background: #FFF; + background: #fff; padding: 4px; font-size: 16px; color: $gl-link-color; @@ -249,7 +256,7 @@ ul.notes { &:hover { background: $gl-info; - color: #FFF; + color: #fff; @include show-add-diff-note; } } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index ecfe0e37c85..260179074cf 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -30,7 +30,7 @@ .btn { line-height: 40px; height: 42px; - padding: 0px 12px; + padding: 0 12px; img { width: 32px; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 1a7b0c1e278..250de079ff5 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; + } } } @@ -63,7 +70,7 @@ .project-repo-buttons { margin-top: 20px; - margin-bottom: 0px; + margin-bottom: 0; .count-buttons { display: block; @@ -118,7 +125,7 @@ left: 1px; margin-top: -9px; border-width: 10px 7px 10px 0; - border-right-color: #FFF; + border-right-color: #fff; } } .count { @@ -140,10 +147,10 @@ cursor: pointer; background-image: none; white-space: nowrap; - margin: 0 11px 0px 4px; + margin: 0 11px 0 4px; &:hover { - background: #FFF; + background: #fff; } } } @@ -188,7 +195,7 @@ } .project_member_row form { - margin: 0px; + margin: 0; } .transfer-project .select2-container { @@ -264,11 +271,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 { @@ -281,11 +288,11 @@ table.table.protected-branches-list tr.no-border { } li { - display:inline; + display: inline; } a { - float:left; + float: left; font-size: 17px; } @@ -440,7 +447,7 @@ pre.light-well { .form-control { @extend .monospace; - background: #FFF; + background: #fff; font-size: 14px; margin-left: -1px; cursor: auto; @@ -450,16 +457,16 @@ pre.light-well { .cannot-be-merged, .cannot-be-merged:hover { - color: #E62958; + color: #e62958; margin-top: 2px; } .private-forks-notice .private-fork-icon { i:nth-child(1) { - color: #2AA056; + color: #2aa056; } i:nth-child(2) { - color: #FFFFFF; + color: #fff; } } diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss index a9111a7388f..eec22c5dc96 100644 --- a/app/assets/stylesheets/pages/runners.scss +++ b/app/assets/stylesheets/pages/runners.scss @@ -1,7 +1,7 @@ .runner-state { padding: 6px 12px; margin-right: 10px; - color: #FFF; + color: #fff; &.runner-state-shared { background: #32b186; diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 84234b15c65..b6e45024644 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -16,7 +16,7 @@ margin-bottom: 20px; input { - border-color: #BBB; + border-color: #bbb; font-weight: bold; } } diff --git a/app/assets/stylesheets/pages/sherlock.scss b/app/assets/stylesheets/pages/sherlock.scss index 92d84d9640f..bed6470dbd3 100644 --- a/app/assets/stylesheets/pages/sherlock.scss +++ b/app/assets/stylesheets/pages/sherlock.scss @@ -13,13 +13,13 @@ table .sherlock-code { } .sherlock-line-samples-table { - margin-bottom: 0px !important; + margin-bottom: 0 !important; thead tr th, tbody tr td { font-size: 13px !important; text-align: right; - padding: 0px 10px !important; + padding: 0 10px !important; } } diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 4b6ef035673..6f777d11641 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -1,7 +1,7 @@ .ci-status { padding: 2px 7px; margin-right: 5px; - border: 1px solid #EEE; + border: 1px solid #eee; white-space: nowrap; @include border-radius(4px); diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 479c3c16d46..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 { @@ -61,7 +44,7 @@ .note-image-attach { margin-top: 4px; - margin-left: 0px; + margin-left: 0; max-width: 200px; float: none; } @@ -89,7 +72,7 @@ .todo-body { margin: 0; - border-left: 2px solid #DDD; + border-left: 2px solid #ddd; padding-left: 10px; } } 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/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss index 05fa9312efb..587bd6a1e8a 100644 --- a/app/assets/stylesheets/pages/ui_dev_kit.scss +++ b/app/assets/stylesheets/pages/ui_dev_kit.scss @@ -7,7 +7,7 @@ .example { &:before { content: "Example"; - color: #BBB; + color: #bbb; } padding: 15px; diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss index 9a50096c0d0..8886c1dff56 100644 --- a/app/assets/stylesheets/pages/xterm.scss +++ b/app/assets/stylesheets/pages/xterm.scss @@ -2,23 +2,23 @@ // color codes are based on http://en.wikipedia.org/wiki/File:Xterm_256color_chart.svg // see also: https://gist.github.com/jasonm23/2868981 - $black: #000000; + $black: #000; $red: #cd0000; $green: #00cd00; $yellow: #cdcd00; - $blue: #0000ee; // according to wikipedia, this is the xterm standard + $blue: #00e; // according to wikipedia, this is the xterm standard //$blue: #1e90ff; // this is used by all the terminals I tried (when configured with the xterm color profile) $magenta: #cd00cd; $cyan: #00cdcd; $white: #e5e5e5; $l-black: #7f7f7f; - $l-red: #ff0000; - $l-green: #00ff00; - $l-yellow: #ffff00; + $l-red: #f00; + $l-green: #0f0; + $l-yellow: #ff0; $l-blue: #5c5cff; - $l-magenta: #ff00ff; - $l-cyan: #00ffff; - $l-white: #ffffff; + $l-magenta: #f0f; + $l-cyan: #0ff; + $l-white: #fff; .term-bold { font-weight: bold; @@ -136,7 +136,7 @@ .xterm-fg-0 { - color: #000000; + color: #000; } .xterm-fg-1 { color: #800000; @@ -163,28 +163,28 @@ color: #808080; } .xterm-fg-9 { - color: #ff0000; + color: #f00; } .xterm-fg-10 { - color: #00ff00; + color: #0f0; } .xterm-fg-11 { - color: #ffff00; + color: #ff0; } .xterm-fg-12 { - color: #0000ff; + color: #00f; } .xterm-fg-13 { - color: #ff00ff; + color: #f0f; } .xterm-fg-14 { - color: #00ffff; + color: #0ff; } .xterm-fg-15 { - color: #ffffff; + color: #fff; } .xterm-fg-16 { - color: #000000; + color: #000; } .xterm-fg-17 { color: #00005f; @@ -199,7 +199,7 @@ color: #0000d7; } .xterm-fg-21 { - color: #0000ff; + color: #00f; } .xterm-fg-22 { color: #005f00; @@ -274,7 +274,7 @@ color: #00d7ff; } .xterm-fg-46 { - color: #00ff00; + color: #0f0; } .xterm-fg-47 { color: #00ff5f; @@ -289,7 +289,7 @@ color: #00ffd7; } .xterm-fg-51 { - color: #00ffff; + color: #0ff; } .xterm-fg-52 { color: #5f0000; @@ -724,7 +724,7 @@ color: #d7ffff; } .xterm-fg-196 { - color: #ff0000; + color: #f00; } .xterm-fg-197 { color: #ff005f; @@ -739,7 +739,7 @@ color: #ff00d7; } .xterm-fg-201 { - color: #ff00ff; + color: #f0f; } .xterm-fg-202 { color: #ff5f00; @@ -814,7 +814,7 @@ color: #ffd7ff; } .xterm-fg-226 { - color: #ffff00; + color: #ff0; } .xterm-fg-227 { color: #ffff5f; @@ -829,7 +829,7 @@ color: #ffffd7; } .xterm-fg-231 { - color: #ffffff; + color: #fff; } .xterm-fg-232 { color: #080808; @@ -850,7 +850,7 @@ color: #3a3a3a; } .xterm-fg-238 { - color: #444444; + color: #444; } .xterm-fg-239 { color: #4e4e4e; @@ -901,6 +901,6 @@ color: #e4e4e4; } .xterm-fg-255 { - color: #eeeeee; + color: #eee; } } diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb index 2463cfa87be..e9b0972bdd8 100644 --- a/app/controllers/admin/abuse_reports_controller.rb +++ b/app/controllers/admin/abuse_reports_controller.rb @@ -6,7 +6,7 @@ class Admin::AbuseReportsController < Admin::ApplicationController def destroy abuse_report = AbuseReport.find(params[:id]) - abuse_report.remove_user if params[:remove_user] + abuse_report.remove_user(deleted_by: current_user) if params[:remove_user] abuse_report.destroy render nothing: true diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index be192964a93..9abf08d0e19 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -119,10 +119,10 @@ class Admin::UsersController < Admin::ApplicationController end def destroy - DeleteUserService.new(current_user).execute(user) + DeleteUserWorker.perform_async(current_user.id, user.id) respond_to do |format| - format.html { redirect_to admin_users_path } + format.html { redirect_to admin_users_path, notice: "The user is being deleted." } format.json { head :ok } end 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/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index 24025d8c723..c721dca58d9 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -7,6 +7,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController if pre_auth.authorizable? if skip_authorization? || matching_token? auth = authorization.authorize + session.delete(:user_return_to) redirect_to auth.redirect_uri else render "doorkeeper/authorizations/new" diff --git a/app/controllers/projects/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 b0a03ee45cc..6603f28a082 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -5,7 +5,7 @@ class Projects::IssuesController < Projects::ApplicationController before_action :issue, only: [:edit, :update, :show] # Allow read any issue - before_action :authorize_read_issue! + before_action :authorize_read_issue!, only: [:show] # Allow write(create) issue before_action :authorize_create_issue!, only: [:new, :create] @@ -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 @@ -127,6 +128,10 @@ class Projects::IssuesController < Projects::ApplicationController end alias_method :subscribable_resource, :issue + def authorize_read_issue! + return render_404 unless can?(current_user, :read_issue, @issue) + end + def authorize_update_issue! return render_404 unless can?(current_user, :update_issue, @issue) end @@ -157,7 +162,7 @@ class Projects::IssuesController < Projects::ApplicationController def issue_params params.require(:issue).permit( - :title, :assignee_id, :position, :description, + :title, :assignee_id, :position, :description, :confidential, :milestone_id, :state_event, :task_num, label_ids: [] ) end diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 40d8098690a..5f471d405f5 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -12,6 +12,13 @@ class Projects::LabelsController < Projects::ApplicationController def index @labels = @project.labels.page(params[:page]).per(PER_PAGE) + + respond_to do |format| + format.html + format.json do + render json: @project.labels + end + end end def new diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 61b82c9db46..7248ede1699 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -5,7 +5,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled before_action :merge_request, only: [ :edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check, - :ci_status, :cancel_merge_when_build_succeeds + :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip ] before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds] before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds] @@ -20,7 +20,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :authorize_create_merge_request!, only: [:new, :create] # Allow modify merge_request - before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :sort] + before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort] def index terms = params['issue_search'] @@ -164,6 +164,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + def remove_wip + MergeRequests::UpdateService.new(project, current_user, title: @merge_request.wipless_title).execute(@merge_request) + + redirect_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), + notice: "The merge request can now be merged." + end + def merge_check @merge_request.check_if_can_be_merged diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index da46731d945..0998b191c07 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -19,7 +19,15 @@ class Projects::MilestonesController < Projects::ApplicationController end @milestones = @milestones.includes(:project) - @milestones = @milestones.page(params[:page]).per(PER_PAGE) + + respond_to do |format| + format.html do + @milestones = @milestones.page(params[:page]).per(PER_PAGE) + end + format.json do + render json: @milestones + end + end end def new diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 36f37221c58..c9930480770 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -134,7 +134,7 @@ class ProjectsController < ApplicationController def autocomplete_sources note_type = params['type'] note_id = params['type_id'] - autocomplete = ::Projects::AutocompleteService.new(@project) + autocomplete = ::Projects::AutocompleteService.new(@project, current_user) participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id) @suggestions = { diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 20a2b0ce8f0..c2befa5a5b3 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -19,4 +19,10 @@ class IssuesFinder < IssuableFinder def klass Issue end + + private + + def init_collection + Issue.visible_to_user(current_user) + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 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 ae4ebc0854a..e00d3204027 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -98,6 +98,10 @@ module IssuesHelper end.sort.to_sentence(last_word_connector: ', or ') end + def confidential_icon(issue) + icon('eye-slash') if issue.confidential? + end + def emoji_icon(name, unicode = nil, aliases = []) unicode ||= Emoji.emoji_filename(name) rescue "" diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 4455dcd0e20..ed37176aa6b 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -32,7 +32,7 @@ module LabelsHelper # link_to_label(label) { "My Custom Label Text" } # # Returns a String - def link_to_label(label, project: nil, type: :issue, &block) + def link_to_label(label, project: nil, type: :issue, tooltip: true, &block) project ||= @project || label.project link = send("namespace_project_#{type.to_s.pluralize}_path", project.namespace, @@ -42,7 +42,7 @@ module LabelsHelper if block_given? link_to link, &block else - link_to render_colored_label(label), link + link_to render_colored_label(label, tooltip: tooltip), link end end @@ -50,23 +50,24 @@ module LabelsHelper @project.labels.pluck(:title) end - def render_colored_label(label, label_suffix = '') + def render_colored_label(label, label_suffix = '', tooltip: true) label_color = label.color || Label::DEFAULT_COLOR text_color = text_color_for_bg(label_color) # Intentionally not using content_tag here so that this method can be called # by LabelReferenceFilter - span = %(<span class="label color-label") + - %(style="background-color: #{label_color}; color: #{text_color}">) + + span = %(<span class="label color-label #{"has_tooltip" if tooltip}" ) + + %(style="background-color: #{label_color}; color: #{text_color}" ) + + %(title="#{escape_once(label.description)}" data-container="body">) + %(#{escape_once(label.name)}#{label_suffix}</span>) span.html_safe end - def render_colored_cross_project_label(label) + def render_colored_cross_project_label(label, tooltip: true) label_suffix = label.project.name_with_namespace label_suffix = " <i>in #{escape_once(label_suffix)}</i>" - render_colored_label(label, label_suffix) + render_colored_label(label, label_suffix, tooltip: tooltip) end def suggested_colors @@ -109,19 +110,12 @@ module LabelsHelper end end - def projects_labels_options - labels = - if @project - @project.labels - else - Label.where(project_id: @projects) - end - - grouped_labels = GlobalLabel.build_collection(labels) - grouped_labels.unshift(Label::None) - grouped_labels.unshift(Label::Any) - - options_from_collection_for_select(grouped_labels, 'name', 'title', params[:label_name]) + def labels_filter_path + if @project + namespace_project_labels_path(@project.namespace, @project, :json) + else + labels_dashboard_path(:json) + end end def label_subscription_status(label) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index e8ac8788d9d..c9d8787bd19 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -38,7 +38,7 @@ module MilestonesHelper def milestone_progress_bar(milestone) options = { class: 'progress-bar progress-bar-success', - style: "width: #{milestone.percent_complete}%;" + style: "width: #{milestone.percent_complete(current_user)}%;" } content_tag :div, class: 'progress' do @@ -46,22 +46,12 @@ module MilestonesHelper end end - def projects_milestones_options - milestones = - if @project - @project.milestones - else - Milestone.where(project_id: @projects) - end.active - - epoch = DateTime.parse('1970-01-01') - grouped_milestones = GlobalMilestone.build_collection(milestones) - grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date } - grouped_milestones.unshift(Milestone::None) - grouped_milestones.unshift(Milestone::Any) - grouped_milestones.unshift(Milestone::Upcoming) - - options_from_collection_for_select(grouped_milestones, 'name', 'title', params[:milestone_title]) + def milestones_filter_dropdown_path + if @project + namespace_project_milestones_path(@project.namespace, @project, :json) + else + milestones_dashboard_path(:json) + end end def milestone_remaining_days(milestone) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index b5acb80b720..5473419ef24 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -26,7 +26,7 @@ module ProjectsHelper image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt:'') if opts[:avatar] end - def link_to_member(project, author, opts = {}) + def link_to_member(project, author, opts = {}, &block) default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" } opts = default_opts.merge(opts) @@ -44,6 +44,8 @@ module ProjectsHelper author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name] end + author_html << capture(&block) if block + author_html = author_html.html_safe if opts[:name] diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 07ddc691d85..edc5686cf08 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -16,14 +16,19 @@ module TodosHelper def todo_target_link(todo) target = todo.target_type.titleize.downcase - link_to "#{target} #{todo.target.to_reference}", todo_target_path(todo), { title: h(todo.target.title) } + link_to "#{target} #{todo.target_reference}", todo_target_path(todo), { title: todo.target.title } end def todo_target_path(todo) anchor = dom_id(todo.note) if todo.note.present? - polymorphic_path([todo.project.namespace.becomes(Namespace), - todo.project, todo.target], anchor: anchor) + if todo.for_commit? + namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project, + todo.target, anchor: anchor) + else + polymorphic_path([todo.project.namespace.becomes(Namespace), + todo.project, todo.target], anchor: anchor) + end end def todos_filter_params diff --git a/app/models/ability.rb b/app/models/ability.rb index 61d5e7dc859..88d7ecf3a16 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 [] @@ -325,6 +327,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 @@ -443,5 +446,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/abuse_report.rb b/app/models/abuse_report.rb index cc59aa4e911..b61f5123127 100644 --- a/app/models/abuse_report.rb +++ b/app/models/abuse_report.rb @@ -19,9 +19,9 @@ class AbuseReport < ActiveRecord::Base validates :message, presence: true validates :user_id, uniqueness: { message: 'has already been reported' } - def remove_user + def remove_user(deleted_by:) user.block - user.destroy + DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true) end def notify 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 5f58c0508fd..5347d4fa1be 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -58,6 +58,13 @@ class Issue < ActiveRecord::Base attributes end + def self.visible_to_user(user) + return where(confidential: false) if user.blank? + return all if user.admin? + + where('issues.confidential = false OR (issues.confidential = true AND (issues.author_id = :user_id OR issues.assignee_id = :user_id OR issues.project_id IN(:project_ids)))', user_id: user.id, project_ids: user.authorized_projects.select(:id)) + end + def self.reference_prefix '#' end @@ -87,11 +94,20 @@ 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 { |branch| branch.end_with?("-#{iid}") } end # Reset issue events cache @@ -120,4 +136,15 @@ class Issue < ActiveRecord::Base note.all_references(current_user).merge_requests end.uniq.select { |mr| mr.open? && mr.closes_issue?(self) } end + + def to_branch_name + "#{title.parameterize}-#{iid}" + 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..a015a9ef394 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -277,8 +277,14 @@ class MergeRequest < ActiveRecord::Base self.target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last end + WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze + def work_in_progress? - !!(title =~ /\A\[?WIP(\]|:| )/i) + title =~ WIP_REGEX + end + + def wipless_title + self.title.sub(WIP_REGEX, "") end def mergeable? @@ -516,11 +522,15 @@ class MergeRequest < ActiveRecord::Base end def target_sha - @target_sha ||= target_project.repository.commit(target_branch).sha + @target_sha ||= target_project.repository.commit(target_branch).try(:sha) end def source_sha - last_commit.try(:sha) + last_commit.try(:sha) || source_tip.try(:sha) + end + + def source_tip + source_branch && source_project.repository.commit(source_branch) end def fetch_ref @@ -568,8 +578,11 @@ class MergeRequest < ActiveRecord::Base end def compute_diverged_commits_count + return 0 unless source_sha && target_sha + Gitlab::Git::Commit.between(target_project.repository.raw_repository, source_sha, target_sha).size end + private :compute_diverged_commits_count def diverged_from_target_branch? diverged_commits_count > 0 diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 374590ba0c5..de7183bf6b4 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -121,8 +121,8 @@ class Milestone < ActiveRecord::Base active? && issues.opened.count.zero? end - def is_empty? - total_items_count.zero? + def is_empty?(user = nil) + total_items_count(user).zero? end def author_id diff --git a/app/models/project.rb b/app/models/project.rb index 3352959a53d..c1374fc9b3c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -576,10 +576,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/project_wiki.rb b/app/models/project_wiki.rb index c96e6f0b8ea..59b1b86d1fb 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -2,7 +2,7 @@ class ProjectWiki include Gitlab::ShellAdapter MARKUPS = { - 'Markdown' => :md, + 'Markdown' => :markdown, 'RDoc' => :rdoc, 'AsciiDoc' => :asciidoc } unless defined?(MARKUPS) @@ -47,7 +47,7 @@ class ProjectWiki def wiki @wiki ||= begin Gollum::Wiki.new(path_to_repo) - rescue Gollum::NoSuchPathError + rescue Rugged::OSError create_repo! end end @@ -90,7 +90,7 @@ class ProjectWiki def create_page(title, content, format = :markdown, message = nil) commit = commit_details(:created, message, title) - wiki.write_page(title, format, content, commit) + wiki.write_page(title, format.to_sym, content, commit) update_project_activity rescue Gollum::DuplicatePageError => e @@ -101,7 +101,7 @@ class ProjectWiki def update_page(page, content, format = :markdown, message = nil) commit = commit_details(:updated, message, page.title) - wiki.update_page(page, page.name, format, content, commit) + wiki.update_page(page, page.name, format.to_sym, content, commit) update_project_activity end diff --git a/app/models/repository.rb b/app/models/repository.rb index 6441cd87e87..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. @@ -758,12 +762,15 @@ class Repository def parse_search_result(result) ref = nil filename = nil + basename = nil startline = 0 result.each_line.each_with_index do |line, index| if line =~ /^.*:.*:\d+:/ ref, filename, startline = line.split(':') startline = startline.to_i - index + extname = File.extname(filename) + basename = filename.sub(/#{extname}$/, '') break end end @@ -776,6 +783,7 @@ class Repository OpenStruct.new( filename: filename, + basename: basename, ref: ref, startline: startline, data: data @@ -853,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/wiki_page.rb b/app/models/wiki_page.rb index dbd70dc5a44..526760779a4 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -62,7 +62,7 @@ class WikiPage # The raw content of this page. def content @attributes[:content] ||= if @page - @page.raw_data + @page.text_data 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/delete_user_service.rb b/app/services/delete_user_service.rb index 173e50c9206..ce79287e35a 100644 --- a/app/services/delete_user_service.rb +++ b/app/services/delete_user_service.rb @@ -5,18 +5,22 @@ class DeleteUserService @current_user = current_user end - def execute(user) - if user.solo_owned_groups.present? + def execute(user, options = {}) + if !options[:delete_solo_owned_groups] && user.solo_owned_groups.present? user.errors[:base] << 'You must transfer ownership or delete groups before you can remove user' - user - else - user.personal_projects.each do |project| - # Skip repository removal because we remove directory with namespace - # that contain all this repositories - ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete! - end + return user + end + + user.solo_owned_groups.each do |group| + DestroyGroupService.new(group, current_user).execute + end - user.destroy + user.personal_projects.each do |project| + # Skip repository removal because we remove directory with namespace + # that contain all this repositories + ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete! end + + user.destroy end end diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb index 9189de390a2..3c42ac61be4 100644 --- a/app/services/destroy_group_service.rb +++ b/app/services/destroy_group_service.rb @@ -6,12 +6,12 @@ class DestroyGroupService end def execute - @group.projects.each do |project| + group.projects.each do |project| # Skip repository removal because we remove directory with namespace # that contain all this repositories ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete! end - @group.destroy + group.destroy end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index d840ab5e340..14e2a2c0699 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/base_service.rb b/app/services/merge_requests/base_service.rb index 7b306a8a531..ac5b58db862 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -5,6 +5,19 @@ module MergeRequests SystemNoteService.change_status(merge_request, merge_request.target_project, current_user, merge_request.state, nil) end + def create_title_change_note(issuable, old_title) + removed_wip = old_title =~ MergeRequest::WIP_REGEX && !issuable.work_in_progress? + added_wip = old_title !~ MergeRequest::WIP_REGEX && issuable.work_in_progress? + + if removed_wip + SystemNoteService.remove_merge_request_wip(issuable, issuable.project, current_user) + elsif added_wip + SystemNoteService.add_merge_request_wip(issuable, issuable.project, current_user) + else + super + end + end + def hook_data(merge_request, action) hook_data = merge_request.to_hook_data(current_user) merge_request_url = Gitlab::UrlBuilder.new(:merge_request).build(merge_request.id) diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 954746a39a5..6e9152e444e 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(/-(\d+)\z/) + 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 58a861ee08e..c644cd0b951 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -144,6 +144,18 @@ class SystemNoteService create_note(noteable: noteable, project: project, author: author, note: body) end + def self.remove_merge_request_wip(noteable, project, author) + body = 'Unmarked this merge request as a Work In Progress' + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + def self.add_merge_request_wip(noteable, project, author) + body = 'Marked this merge request as a **Work In Progress**' + + create_note(noteable: noteable, project: project, author: author, note: body) + end + # Called when the title of a Noteable is changed # # noteable - Noteable object that responds to `title` @@ -207,6 +219,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 `issue-branch-button-201`" + def self.new_issue_branch(issue, project, author, branch) + h = Gitlab::Application.routes.url_helpers + link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch) + + 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/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml index 5736a301910..f417b2e44a4 100644 --- a/app/views/admin/labels/_label.html.haml +++ b/app/views/admin/labels/_label.html.haml @@ -1,6 +1,6 @@ %li{id: dom_id(label)} .label-row - = render_colored_label(label) + = render_colored_label(label, tooltip: false) = markdown(label.description, pipeline: :single_line) .pull-right = link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm' diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index 45cfe3da188..e3a4d64df01 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -2,7 +2,7 @@ .todo-item.todo-block = image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:'' - .todo-title + .todo-title.title %span.author-name - if todo.author = link_to_author(todo) @@ -16,7 +16,9 @@ - if todo.pending? .todo-actions.pull-right - = link_to 'Done', [:dashboard, todo], method: :delete, class: 'btn' + = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do + Done + = icon('spinner spin') .todo-body .todo-note diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 946d7df3933..f9ec3a89158 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -3,13 +3,15 @@ .top-area %ul.nav-links - %li{class: ('active' if params[:state].blank? || params[:state] == 'pending')} + - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending') + %li{class: "todos-pending #{todo_pending_active}"} = link_to todos_filter_path(state: 'pending') do %span To do %span{class: 'badge'} = todos_pending_count - %li{class: ('active' if params[:state] == 'done')} + - todo_done_active = ('active' if params[:state] == 'done') + %li{class: "todos-done #{todo_done_active}"} = link_to todos_filter_path(state: 'done') do %span Done @@ -18,7 +20,9 @@ .nav-controls - if @todos.any?(&:pending?) - = link_to 'Mark all as done', destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn', method: :delete + = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete do + Mark all as done + = icon('spinner spin') .todos-filters .gray-content-block.second-block @@ -42,12 +46,12 @@ .prepend-top-default - if @todos.any? - @todos.group_by(&:project).each do |group| - .panel.panel-default.panel-small + .panel.panel-default.panel-small.js-todos-list - project = group[0] .panel-heading = link_to project.name_with_namespace, namespace_project_path(project.namespace, project) - %ul.well-list.todos-list + %ul.content-list.todos-list = render group[1] = paginate @todos, theme: "gitlab" - else diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 36fb2d51629..2d9d9dd6342 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -1,4 +1,4 @@ -- if event.proper? +- if event.proper?(current_user) .event-item{class: "#{event.body? ? "event-block" : "event-inline" }"} .event-item-timestamp #{time_ago_with_tooltip(event.created_at)} diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 714da410f56..bfa5937cf3f 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -45,6 +45,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/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 6086ad3661e..2e1a37aa06d 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -20,4 +20,4 @@ - next unless blob = render 'projects/diffs/file', i: index, project: project, - diff_file: diff_file, diff_commit: diff_commit, blob: blob + diff_file: diff_file, diff_commit: diff_commit, blob: blob, diff_refs: diff_refs diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 3ac058a3bf8..3898bb202c5 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -42,13 +42,17 @@ .diff-content.diff-wrap-lines -# Skipp all non non-supported blobs - return unless blob.respond_to?('text?') - - if blob_text_viewable?(blob) - - if diff_view == 'parallel' - = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i - - else - = render "projects/diffs/text_file", diff_file: diff_file, index: i - - elsif blob.image? - - old_file = project.repository.prev_blob_for_diff(diff_commit, diff_file) - = render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i + - if diff_file.too_large? + .nothing-here-block + This diff could not be displayed because it is too large. - else - .nothing-here-block No preview for this file type + - if blob_text_viewable?(blob) + - if diff_view == 'parallel' + = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i + - else + = render "projects/diffs/text_file", diff_file: diff_file, index: i + - elsif blob.image? + - old_file = project.repository.prev_blob_for_diff(diff_commit, diff_file) + = render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i, diff_refs: diff_refs + - else + .nothing-here-block No preview for this file type diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml index 752e92e2e6b..8367112a9cb 100644 --- a/app/views/projects/diffs/_image.html.haml +++ b/app/views/projects/diffs/_image.html.haml @@ -1,6 +1,7 @@ - diff = diff_file.diff - file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, diff.new_path)) -- old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.parent_id, diff.old_path)) +- old_commit_id = diff_refs.first.id +- old_file_raw_path = namespace_project_raw_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path)) - if diff.renamed_file || diff.new_file || diff.deleted_file .image %span.wrap @@ -12,7 +13,7 @@ %div.two-up.view %span.wrap .frame.deleted - %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(@commit.parent_id, diff.old_path))} + %a{href: namespace_project_blob_path(@project.namespace, @project, tree_join(old_commit_id, diff.old_path))} %img{src: old_file_raw_path} %p.image-info.hide %span.meta-filesize= "#{number_to_human_size old_file.size}" diff --git a/app/views/projects/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/merge_requests/widget/open/_wip.html.haml b/app/views/projects/merge_requests/widget/open/_wip.html.haml index 0cf16542cc1..c296422a9cf 100644 --- a/app/views/projects/merge_requests/widget/open/_wip.html.haml +++ b/app/views/projects/merge_requests/widget/open/_wip.html.haml @@ -1,5 +1,11 @@ %h4 This merge request is currently a Work In Progress -%p - When this merge request is ready, remove the "WIP" prefix from the title to allow it to be merged. +- if can?(current_user, :update_merge_request, @merge_request) + %p + When this merge request is ready, + = link_to remove_wip_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), method: :post do + remove the + %code WIP: + prefix from the title + to allow it to be merged. diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index b4597043a27..be63875ab34 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -42,7 +42,7 @@ = preserve do = markdown @milestone.description -- if @milestone.complete? && @milestone.active? +- if @milestone.complete?(current_user) && @milestone.active? .alert.alert-success.prepend-top-default %span All issues for this milestone are closed. You may close milestone now. diff --git a/app/views/projects/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/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml index f5859481d46..235106c4f74 100644 --- a/app/views/search/results/_wiki_blob.html.haml +++ b/app/views/search/results/_wiki_blob.html.haml @@ -2,9 +2,9 @@ .blob-result .file-holder .file-title - = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_blob.filename) do + = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_blob.basename) do %i.fa.fa-file %strong - = wiki_blob.filename + = wiki_blob.basename .file-content.code.term = render 'shared/file_highlight', blob: wiki_blob, first_line_number: wiki_blob.startline diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index 8134b15d245..4b47b0291be 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -1,4 +1,4 @@ %span.label-row - = link_to_label(label) + = link_to_label(label, tooltip: false) %span.prepend-left-10 = markdown(label.description, pipeline: :single_line) diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index 82eeb2088c8..67a59f420f3 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 @@ -25,12 +25,13 @@ = visibility_level_icon(group.visibility_level, fw: false) = 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 42a3c2c3f02..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 Author", 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 d5a4aad05d9..80418e69d9c 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -13,12 +13,21 @@ - if issuable.is_a?(MergeRequest) %p.help-block - - if issuable.work_in_progress? - Remove the <code>WIP</code> prefix from the title to allow this - <strong>Work In Progress</strong> merge request to be merged when it's ready. - - else - Start the title with <code>[WIP]</code> or <code>WIP:</code> to prevent a - <strong>Work In Progress</strong> merge request from being merged before it's ready. + .js-wip-explanation + %a.js-toggle-wip{href: ""} + Remove the + %code WIP: + prefix from the title + to allow this + %strong Work In Progress + merge request to be merged when it's ready. + .js-no-wip-explanation + %a.js-toggle-wip{href: ""} + Start the title with + %code WIP: + to prevent a + %strong Work In Progress + merge request from being merged before it's ready. .form-group.detail-page-description = f.label :description, 'Description', class: 'control-label' .col-sm-10 @@ -29,6 +38,15 @@ = render 'projects/notes/hints' .clearfix .error-alert + +- if issuable.is_a?(Issue) && !issuable.project.private? + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :confidential do + = f.check_box :confidential + This issue is confidential and should only be visible to team members + - if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) %hr .form-group diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml new file mode 100644 index 00000000000..87617315181 --- /dev/null +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -0,0 +1,39 @@ +- if params[:label_name] + = hidden_field_tag(:label_name, params[:label_name]) +.dropdown + %button.dropdown-menu-toggle.js-label-select.js-filter-submit{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}} + %span.dropdown-toggle-text + = h(params[:label_name].presence || "Label") + = icon('chevron-down') + .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable + .dropdown-page-one + = dropdown_title("Filter by label") + = dropdown_filter("Search labels") + = dropdown_content + - if @project + = dropdown_footer do + %ul.dropdown-footer-list + - if can? current_user, :admin_label, @project + %li + %a.dropdown-toggle-page{href: "#"} + Create new + %li + = link_to namespace_project_labels_path(@project.namespace, @project) do + - if can? current_user, :admin_label, @project + Manage labels + - else + View labels + - if can? current_user, :admin_label, @project and @project + .dropdown-page-two + = dropdown_title("Create new label", back: true) + = dropdown_content do + %input#new_label_color{type: "hidden"} + %input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"} + .dropdown-label-color-preview.js-dropdown-label-color-preview + .suggest-colors.suggest-colors-dropdown + - suggested_colors.each do |color| + = link_to '#', style: "background-color: #{color}", data: { color: color } do +   + %button.btn.btn-primary.js-new-label-btn{type: "button"} + Create + = dropdown_loading diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml new file mode 100644 index 00000000000..0434506c8d7 --- /dev/null +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -0,0 +1,16 @@ +- if params[:milestone_title] + = hidden_field_tag(:milestone_title, params[:milestone_title]) += dropdown_tag(h(params[:milestone_title].presence || "Milestone"), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable", + placeholder: "Search milestones", footer_content: @project.present?, data: { show_no: true, show_any: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do + - if @project + %ul.dropdown-footer-list + - if can? current_user, :admin_milestone, @project + %li + = link_to new_namespace_project_milestone_path(@project.namespace, @project), title: "New Milestone" do + Create new + %li + = link_to namespace_project_milestones_path(@project.namespace, @project) do + - if can? current_user, :admin_milestone, @project + Manage milestones + - else + View milestones diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml index f1d92ef48b2..3fb409ff727 100644 --- a/app/views/shared/issuable/_participants.html.haml +++ b/app/views/shared/issuable/_participants.html.haml @@ -1,3 +1,6 @@ +- participants_row = 7 +- participants_size = participants.size +- participants_extra = participants_size - participants_row .block.participants .sidebar-collapsed-icon = icon('users') @@ -5,6 +8,13 @@ = participants.count .title.hide-collapsed = pluralize participants.count, "participant" - - participants.each do |participant| - %span.hide-collapsed - = link_to_member(@project, participant, name: false, size: 24) + .hide-collapsed.participants-list + - participants.each do |participant| + .participants-author.js-participants-author + = link_to_member(@project, participant, name: false, size: 24) + - if participants_extra > 0 + %div.participants-more + %a.js-participants-more{href: "#", data: {original_text: "+ #{participants_size - 7} more", less_text: "- show less"}} + + #{participants_extra} more +:javascript + Issue.prototype.PARTICIPANTS_ROW_COUNT = #{participants_row}; diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 23b1ed1e51b..2b95b19facc 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -1,13 +1,12 @@ %aside.right-sidebar{ class: sidebar_gutter_collapsed_class } .issuable-sidebar - .block + .block.issuable-sidebar-header %span.issuable-count.hide-collapsed.pull-left = issuable.iid of = issuables_count(issuable) - %span.pull-right - %a.gutter-toggle.js-sidebar-toggle{href: '#'} - = sidebar_gutter_toggle_icon + %a.gutter-toggle.pull-right.js-sidebar-toggle{href: '#'} + = sidebar_gutter_toggle_icon .issuable-nav.hide-collapsed.pull-right.btn-group{role: 'group', "aria-label" => '...'} - if prev_issuable = prev_issuable_for(issuable) = link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn' @@ -22,20 +21,20 @@ = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f| .block.assignee - .sidebar-collapsed-icon + .sidebar-collapsed-icon.sidebar-collapsed-user{data: {toggle: "tooltip", placement: "left", container: "body"}, title: (issuable.assignee.to_reference if issuable.assignee)} - if issuable.assignee - = link_to_member_avatar(issuable.assignee, size: 24) + = link_to_member(@project, issuable.assignee, size: 24) - else = icon('user') .title.hide-collapsed - %label - Assignee + Assignee - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - .pull-right - = link_to 'Edit', '#', class: 'edit-link' - .value.hide-collapsed + = link_to 'Edit', '#', class: 'edit-link pull-right' + .value.bold.hide-collapsed - if issuable.assignee - %strong= link_to_member(@project, issuable.assignee, size: 24) + = link_to_member(@project, issuable.assignee, size: 32) do + %span.username + = issuable.assignee.to_reference - if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee) %a.pull-right.cannot-be-merged{href: '#', data: {toggle: 'tooltip'}, title: 'Not allowed to merge'} = icon('exclamation-triangle') @@ -54,18 +53,13 @@ - else No .title.hide-collapsed - %label - Milestone + Milestone - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - .pull-right - = link_to 'Edit', '#', class: 'edit-link' - .value.hide-collapsed + = link_to 'Edit', '#', class: 'edit-link pull-right' + .value.bold.hide-collapsed - if issuable.milestone - %span.back-to-milestone - = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do - %strong - = icon('clock-o') - = issuable.milestone.title + = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do + = issuable.milestone.title - else .light None .selectbox.hide-collapsed @@ -80,11 +74,10 @@ %span = issuable.labels.count .title.hide-collapsed - %label Labels + Labels - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - .pull-right - = link_to 'Edit', '#', class: 'edit-link' - .value.issuable-show-labels.hide-collapsed + = link_to 'Edit', '#', class: 'edit-link pull-right' + .value.issuable-show-labels.hide-collapsed{class: ("has-labels" if issuable.labels.any?)} - if issuable.labels.any? - issuable.labels.each do |label| = link_to_label(label, type: issuable.to_ability_name) @@ -95,14 +88,13 @@ { selected: issuable.label_ids }, multiple: true, class: 'select2 js-select2', data: { placeholder: "Select labels" } = render "shared/issuable/participants", participants: issuable.participants(current_user) - %hr - if current_user - subscribed = issuable.subscribed?(current_user) .block.light.subscription{data: {url: toggle_subscription_path(issuable)}} .sidebar-collapsed-icon = icon('rss') .title.hide-collapsed - %label.light Notifications + Notifications - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed' %button.btn.btn-block.btn-gray.subscribe-button.hide-collapsed{:type => 'button'} %span= subscribed ? 'Unsubscribe' : 'Subscribe' diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml index f7c6fc14adf..85888096722 100644 --- a/app/views/shared/milestones/_issuable.html.haml +++ b/app/views/shared/milestones/_issuable.html.haml @@ -10,6 +10,8 @@ %strong #{project.name} · - elsif show_full_project_name %strong #{project.name_with_namespace} · + - if issuable.is_a?(Issue) + = confidential_icon(issuable) = link_to_gfm issuable.title, [project.namespace.becomes(Namespace), project, issuable], title: issuable.title %div{class: 'issuable-detail'} = link_to [project.namespace.becomes(Namespace), project, issuable] do diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml index ba27bafd1bc..868b2357003 100644 --- a/app/views/shared/milestones/_labels_tab.html.haml +++ b/app/views/shared/milestones/_labels_tab.html.haml @@ -5,7 +5,7 @@ %li %span.label-row = link_to milestones_label_path(options) do - - render_colored_label(label) + - render_colored_label(label, tooltip: false) %span.prepend-left-10 = markdown(label.description, pipeline: :single_line) diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index f01138af3f0..6b25745c554 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -6,10 +6,10 @@ .col-sm-6 %strong= link_to_gfm truncate(milestone.title, length: 100), milestone_path .col-sm-6 - .pull-right.light #{milestone.percent_complete}% complete + .pull-right.light #{milestone.percent_complete(current_user)}% complete .row .col-sm-6 - = link_to pluralize(milestone.issues.size, 'Issue'), issues_path + = link_to pluralize(milestone.issues_visible_to_user(current_user).size, 'Issue'), issues_path · = link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path .col-sm-6= milestone_progress_bar(milestone) diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml index 59d4ae29f79..385c6596606 100644 --- a/app/views/shared/milestones/_summary.html.haml +++ b/app/views/shared/milestones/_summary.html.haml @@ -3,15 +3,15 @@ .context.prepend-top-default .milestone-summary %h4 Progress - %strong= milestone.issues.size + %strong= milestone.issues_visible_to_user(current_user).size issues: %span.milestone-stat - %strong= milestone.issues.opened.size + %strong= milestone.issues_visible_to_user(current_user).opened.size open and - %strong= milestone.issues.closed.size + %strong= milestone.issues_visible_to_user(current_user).closed.size closed %span.milestone-stat - %strong== #{milestone.percent_complete}% + %strong== #{milestone.percent_complete(current_user)}% complete %span.milestone-stat diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml index 57d7ee85a3b..2b6ce2d7e7a 100644 --- a/app/views/shared/milestones/_tabs.html.haml +++ b/app/views/shared/milestones/_tabs.html.haml @@ -2,7 +2,7 @@ %li.active = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do Issues - %span.badge= milestone.issues.size + %span.badge= milestone.issues_visible_to_user(current_user).size %li = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do Merge Requests @@ -21,7 +21,7 @@ .tab-content.milestone-content .tab-pane.active#tab-issues - = render 'shared/milestones/issues_tab', issues: milestone.issues, show_project_name: show_project_name, show_full_project_name: show_full_project_name + = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user), show_project_name: show_project_name, show_full_project_name: show_full_project_name .tab-pane#tab-merge-requests = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name .tab-pane#tab-participants diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml index 4cf1d948b5b..cab8743a077 100644 --- a/app/views/shared/milestones/_top.html.haml +++ b/app/views/shared/milestones/_top.html.haml @@ -28,7 +28,7 @@ %h2.title = markdown escape_once(milestone.title), pipeline: :single_line -- if milestone.complete? && milestone.active? +- if milestone.complete?(current_user) && milestone.active? .alert.alert-success.prepend-top-default - close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.' %span All issues for this milestone are closed. #{close_msg} @@ -47,7 +47,7 @@ - project_name = group ? ms.project.name : ms.project.name_with_namespace = link_to project_name, namespace_project_milestone_path(ms.project.namespace, ms.project, ms) %td - = ms.issues.opened.count + = ms.issues_visible_to_user(current_user).opened.count %td - if ms.closed? Closed diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 97cfb76cdb0..872d2bdf46d 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -7,26 +7,11 @@ - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description - ci_commit = project.ci_commit(project.commit.sha) if ci && !project.empty_repo? && project.commit -- cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.2'] +- cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.3'] - cache_key.push(ci_commit.status) if ci_commit %li.project-row{ class: css_class } = cache(cache_key) do - = link_to project_path(project), class: dom_class(project) do - - if avatar - .dash-project-avatar - - if use_creator_avatar - = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:'' - - else - = project_icon(project, alt: '', class: 'avatar project-avatar s40') - %span.project-full-name.title - %span.namespace-name - - if project.namespace && !skip_namespace - = project.namespace.human_name - \/ - %span.project-name.filter-title - = project.name - .controls - if project.main_language %span @@ -45,6 +30,23 @@ %span.visibility-icon.has_tooltip{data: { container: 'body', placement: 'left' }, title: "#{visibility_level_label(project.visibility_level)} - #{project_visibility_level_description(project.visibility_level)}"} = visibility_level_icon(project.visibility_level, fw: false) + + .title + = link_to project_path(project), class: dom_class(project) do + - if avatar + .dash-project-avatar + - if use_creator_avatar + = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:'' + - else + = project_icon(project, alt: '', class: 'avatar project-avatar s40') + %span.project-full-name + %span.namespace-name + - if project.namespace && !skip_namespace + = project.namespace.human_name + \/ + %span.project-name.filter-title + = project.name + - if show_last_commit_as_description .description = link_to_gfm project.commit.title, namespace_project_commit_path(project.namespace, project, project.commit), diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml index a316a085107..c96dfefe17f 100644 --- a/app/views/shared/snippets/_snippet.html.haml +++ b/app/views/shared/snippets/_snippet.html.haml @@ -1,8 +1,8 @@ %li.snippet-row = image_tag avatar_icon(snippet.author_email), class: "avatar s40 hidden-xs", alt: '' - .snippet-title - = link_to reliable_snippet_path(snippet), class: 'title' do + .title + = link_to reliable_snippet_path(snippet) do = truncate(snippet.title, length: 60) - if snippet.private? %span.label.label-gray diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb new file mode 100644 index 00000000000..6ff361e4d80 --- /dev/null +++ b/app/workers/delete_user_worker.rb @@ -0,0 +1,10 @@ +class DeleteUserWorker + include Sidekiq::Worker + + def perform(current_user_id, delete_user_id, options = {}) + delete_user = User.find(delete_user_id) + current_user = User.find(current_user_id) + + DeleteUserService.new(current_user).execute(delete_user, options.symbolize_keys) + end +end 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 diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 05f127d622a..500b745f55e 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -357,6 +357,12 @@ production: &base # crowd_server_url: 'CROWD SERVER URL', # application_name: 'YOUR_APP_NAME', # application_password: 'YOUR_APP_PASSWORD' } } + # + # - { name: 'auth0', + # args: { + # client_id: 'YOUR_AUTH0_CLIENT_ID', + # client_secret: 'YOUR_AUTH0_CLIENT_SECRET', + # namespace: 'YOUR_AUTH0_DOMAIN' } } # SSO maximum session duration in seconds. Defaults to CAS default of 8 hours. # cas3: diff --git a/config/initializers/gollum.rb b/config/initializers/gollum.rb new file mode 100644 index 00000000000..703f24f93b2 --- /dev/null +++ b/config/initializers/gollum.rb @@ -0,0 +1,13 @@ +module Gollum + GIT_ADAPTER = "rugged" +end +require "gollum-lib" + +module Gollum + class Committer + # Patch for UTF-8 path + def method_missing(name, *args) + index.send(name, *args) + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 2ae282f48a6..561987322b2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -351,6 +351,8 @@ Rails.application.routes.draw do get :issues get :merge_requests get :activity + get :labels + get :milestones scope module: :dashboard do resources :milestones, only: [:index, :show] @@ -621,6 +623,7 @@ Rails.application.routes.draw do post :cancel_merge_when_build_succeeds get :ci_status post :toggle_subscription + post :remove_wip end collection do diff --git a/db/migrate/20160223192159_add_confidential_to_issues.rb b/db/migrate/20160223192159_add_confidential_to_issues.rb new file mode 100644 index 00000000000..e9d47fd589a --- /dev/null +++ b/db/migrate/20160223192159_add_confidential_to_issues.rb @@ -0,0 +1,6 @@ +class AddConfidentialToIssues < ActiveRecord::Migration + def change + add_column :issues, :confidential, :boolean, default: false + add_index :issues, :confidential + end +end diff --git a/db/migrate/20160316123110_ci_runners_token_index.rb b/db/migrate/20160316123110_ci_runners_token_index.rb new file mode 100644 index 00000000000..67bf5b4f978 --- /dev/null +++ b/db/migrate/20160316123110_ci_runners_token_index.rb @@ -0,0 +1,13 @@ +class CiRunnersTokenIndex < ActiveRecord::Migration + disable_ddl_transaction! + + def change + args = [:ci_runners, :token] + + if Gitlab::Database.postgresql? + args << { algorithm: :concurrently } + end + + add_index(*args) + end +end diff --git a/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb b/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb new file mode 100644 index 00000000000..6871b3920df --- /dev/null +++ b/db/migrate/20160316192622_change_target_id_to_null_on_todos.rb @@ -0,0 +1,5 @@ +class ChangeTargetIdToNullOnTodos < ActiveRecord::Migration + def change + change_column_null :todos, :target_id, true + end +end diff --git a/db/migrate/20160316204731_add_commit_id_to_todos.rb b/db/migrate/20160316204731_add_commit_id_to_todos.rb new file mode 100644 index 00000000000..ae19fdd1abd --- /dev/null +++ b/db/migrate/20160316204731_add_commit_id_to_todos.rb @@ -0,0 +1,6 @@ +class AddCommitIdToTodos < ActiveRecord::Migration + def change + add_column :todos, :commit_id, :string + add_index :todos, :commit_id + end +end diff --git a/db/schema.rb b/db/schema.rb index f1bccd62745..719fedcdbfb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160314143402) do +ActiveRecord::Schema.define(version: 20160316204731) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -261,6 +261,7 @@ ActiveRecord::Schema.define(version: 20160314143402) do end add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} + add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree add_index "ci_runners", ["token"], name: "index_ci_runners_on_token_trigram", using: :gin, opclasses: {"token"=>"gin_trgm_ops"} create_table "ci_services", force: :cascade do |t| @@ -416,10 +417,12 @@ ActiveRecord::Schema.define(version: 20160314143402) do t.string "state" t.integer "iid" t.integer "updated_by_id" + t.boolean "confidential", default: false end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree + add_index "issues", ["confidential"], name: "index_issues_on_confidential", using: :btree add_index "issues", ["created_at", "id"], name: "index_issues_on_created_at_and_id", using: :btree add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"} @@ -866,7 +869,7 @@ ActiveRecord::Schema.define(version: 20160314143402) do create_table "todos", force: :cascade do |t| t.integer "user_id", null: false t.integer "project_id", null: false - t.integer "target_id", null: false + t.integer "target_id" t.string "target_type", null: false t.integer "author_id" t.integer "action", null: false @@ -874,9 +877,11 @@ ActiveRecord::Schema.define(version: 20160314143402) do t.datetime "created_at" t.datetime "updated_at" t.integer "note_id" + t.string "commit_id" end add_index "todos", ["author_id"], name: "index_todos_on_author_id", using: :btree + add_index "todos", ["commit_id"], name: "index_todos_on_commit_id", using: :btree add_index "todos", ["note_id"], name: "index_todos_on_note_id", using: :btree add_index "todos", ["project_id"], name: "index_todos_on_project_id", using: :btree add_index "todos", ["state"], name: "index_todos_on_state", using: :btree diff --git a/doc/README.md b/doc/README.md index db19c3de8d1..08d0a6a5bfb 100644 --- a/doc/README.md +++ b/doc/README.md @@ -3,6 +3,7 @@ ## User documentation - [API](api/README.md) Automate GitLab via a simple and powerful API. +- [CI](ci/README.md) - [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. - [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab. - [Importing to GitLab](workflow/importing/README.md). @@ -16,42 +17,6 @@ - [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project. - [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. -## CI User documentation - -- [Get started with GitLab CI](ci/quick_start/README.md) -- [Learn how to enable or disable GitLab CI](ci/enable_or_disable_ci.md) -- [Learn how `.gitlab-ci.yml` works](ci/yaml/README.md) -- [Configure a Runner, the application that runs your builds](ci/runners/README.md) -- [Use Docker images with GitLab Runner](ci/docker/using_docker_images.md) -- [Use CI to build Docker images](ci/docker/using_docker_build.md) -- [Use variables in your `.gitlab-ci.yml`](ci/variables/README.md) -- [Use SSH keys in your build environment](ci/ssh_keys/README.md) -- [Trigger builds through the API](ci/triggers/README.md) -- [Build artifacts](ci/build_artifacts/README.md) -- [User permissions](ci/permissions/README.md) -- [API](ci/api/README.md) - -### CI Examples - -- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml) -- [Test your PHP applications](ci/examples/php.md) -- [Test and deploy Ruby applications to Heroku](ci/examples/test-and-deploy-ruby-application-to-heroku.md) -- [Test and deploy Python applications to Heroku](ci/examples/test-and-deploy-python-application-to-heroku.md) -- [Test Clojure applications](ci/examples/test-clojure-application.md) -- [Using `dpl` as deployment tool](ci/deployment/README.md) -- Help your favorite programming language and GitLab by sending a merge request - with a guide for that language. - -### CI Services - -GitLab CI uses the `services` keyword to define what docker containers should -be linked with your base image. Below is a list of examples you may use: - -- [Using MySQL](ci/services/mysql.md) -- [Using PostgreSQL](ci/services/postgres.md) -- [Using Redis](ci/services/redis.md) -- [Using Other Services](ci/docker/using_docker_images.md#how-to-use-other-images-as-services) - ## Administrator documentation - [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when webhooks aren't enough. diff --git a/doc/api/users.md b/doc/api/users.md index 44a29da5ecc..383e7c76ab0 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -220,6 +220,7 @@ Parameters: - `bio` - User's biography - `admin` (optional) - User is admin - true or false (default) - `can_create_group` (optional) - User can create groups - true or false +- `external` (optional) - Flags the user as external - true or false(default) Note, at the moment this method does only return a 404 error, even in cases where a 409 (Conflict) would be more appropriate, diff --git a/doc/ci/deployment/README.md b/doc/ci/deployment/README.md index ffd841ca9e7..7d91ce6710f 100644 --- a/doc/ci/deployment/README.md +++ b/doc/ci/deployment/README.md @@ -89,7 +89,7 @@ We also use two secure variables: In GitLab CI 7.12 a new feature was introduced: Secure Variables. Secure Variables can added by going to `Project > Variables > Add Variable`. **This feature requires `gitlab-runner` with version equal or greater than 0.4.0.** -The variables that are defined in the project settings are send along with the build script to the runner. +The variables that are defined in the project settings are sent along with the build script to the runner. The secure variables are stored out of the repository. Never store secrets in your projects' .gitlab-ci.yml. It is also important that secret's value is hidden in the build log. diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 5158e3c387c..762b35859b9 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -135,6 +135,9 @@ thus allowing to fine tune them. ### cache +>**Note:** +Introduced in GitLab Runner v0.7.0. + `cache` is used to specify a list of files and directories which should be cached between builds. @@ -143,15 +146,55 @@ cached between builds. If `cache` is defined outside the scope of the jobs, it means it is set globally and all jobs will use its definition. -To cache all git untracked files and files in `binaries`: +Cache all files in `binaries` and `.config`: + +```yaml +rspec: + script: test + cache: + paths: + - binaries/ + - .config +``` + +Cache all Git untracked files: + +```yaml +rspec: + script: test + cache: + untracked: true +``` + +Cache all Git untracked files and files in `binaries`: + +```yaml +rspec: + script: test + cache: + untracked: true + paths: + - binaries/ +``` + +Locally defined cache overwrites globally defined options. This will cache only +`binaries/`: ```yaml cache: - untracked: true paths: - - binaries/ + - my/files + +rspec: + script: test + cache: + paths: + - binaries/ ``` +The cache is provided on best effort basis, so don't expect that cache will be +always present. For implementation details please check GitLab Runner. + #### cache:key >**Note:** @@ -236,6 +279,8 @@ job_name: | Keyword | Required | Description | |---------------|----------|-------------| | script | yes | Defines a shell script which is executed by runner | +| image | no | Use docker image, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) | +| services | no | Use docker services, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) | | stage | no | Defines a build stage (default: `test`) | | type | no | Alias for `stage` | | only | no | Defines a list of git refs for which build is created | @@ -418,14 +463,14 @@ artifacts: - .config ``` -Send all git untracked files: +Send all Git untracked files: ```yaml artifacts: untracked: true ``` -Send all git untracked files and files in `binaries`: +Send all Git untracked files and files in `binaries`: ```yaml artifacts: @@ -579,63 +624,6 @@ deploy: script: make deploy ``` -### cache - ->**Note:** -Introduced in GitLab Runner v0.7.0. - -`cache` is used to specify list of files and directories which should be cached -between builds. Below are some examples: - -Cache all files in `binaries` and `.config`: - -```yaml -rspec: - script: test - cache: - paths: - - binaries/ - - .config -``` - -Cache all git untracked files: - -```yaml -rspec: - script: test - cache: - untracked: true -``` - -Cache all git untracked files and files in `binaries`: - -```yaml -rspec: - script: test - cache: - untracked: true - paths: - - binaries/ -``` - -Locally defined cache overwrites globally defined options. This will cache only -`binaries/`: - -```yaml -cache: - paths: - - my/files - -rspec: - script: test - cache: - paths: - - binaries/ -``` - -The cache is provided on best effort basis, so don't expect that cache will be -always present. For implementation details please check GitLab Runner. - ## Hidden jobs >**Note:** diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md index 15051dd76f9..dcdf49d3379 100644 --- a/doc/hooks/custom_hooks.md +++ b/doc/hooks/custom_hooks.md @@ -2,7 +2,7 @@ **Note: Custom git hooks must be configured on the filesystem of the GitLab server. Only GitLab server administrators will be able to complete these tasks. -Please explore [webhooks](doc/web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).** +Please explore [webhooks](../web_hooks/web_hooks.md) as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).** Git natively supports hooks that are executed on different actions. Examples of server-side git hooks include pre-receive, post-receive, and update. diff --git a/doc/install/installation.md b/doc/install/installation.md index 4f011397269..c567846f624 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -22,7 +22,7 @@ If the highest number stable branch is unclear please check the [GitLab Blog](ht This guide is long because it covers many cases and includes all commands you need, this is [one of the few installation scripts that actually works out of the box](https://twitter.com/robinvdvleuten/status/424163226532986880). -This installation guide was created for and tested on **Debian/Ubuntu** operating systems. Please read [doc/install/requirements.md](./requirements.md) for hardware and operating system requirements. If you want to install on RHEL/CentOS we recommend using the [Omnibus packages](https://about.gitlab.com/downloads/). +This installation guide was created for and tested on **Debian/Ubuntu** operating systems. Please read [requirements.md](requirements.md) for hardware and operating system requirements. If you want to install on RHEL/CentOS we recommend using the [Omnibus packages](https://about.gitlab.com/downloads/). This is the official installation guide to set up a production server. To set up a **development installation** or for many other installation options please see [the installation section of the readme](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/README.md#installation). @@ -76,7 +76,7 @@ Make sure you have the right version of Git installed # Install Git sudo apt-get install -y git-core - # Make sure Git is version 1.7.10 or higher, for example 1.7.12 or 2.0.0 + # Make sure Git is version 2.7.4 or higher git --version Is the system packaged Git too old? Remove it and compile from source. @@ -89,8 +89,9 @@ Is the system packaged Git too old? Remove it and compile from source. # Download and compile from source cd /tmp - curl -L --progress https://www.kernel.org/pub/software/scm/git/git-2.4.3.tar.gz | tar xz - cd git-2.4.3/ + curl -O --progress https://www.kernel.org/pub/software/scm/git/git-2.7.4.tar.gz + echo '7104c4f5d948a75b499a954524cb281fe30c6649d8abe20982936f75ec1f275b git-2.7.4.tar.gz' | shasum -a256 -c - && tar -xzf git-2.7.4.tar.gz + cd git-2.7.4/ ./configure make prefix=/usr/local all @@ -143,7 +144,7 @@ use 64-bit Linux. You can find downloads for other platforms at the [Go download page](https://golang.org/dl). curl -O --progress https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz - echo '43afe0c5017e502630b1aea4d44b8a7f059bf60d7f29dfd58db454d4e4e0ae53 go1.5.3.linux-amd64.tar.gz' | shasum -c - && \ + echo '43afe0c5017e502630b1aea4d44b8a7f059bf60d7f29dfd58db454d4e4e0ae53 go1.5.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ sudo tar -C /usr/local -xzf go1.5.3.linux-amd64.tar.gz sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ rm go1.5.3.linux-amd64.tar.gz @@ -161,18 +162,11 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da # Install the database packages sudo apt-get install -y postgresql postgresql-client libpq-dev - # Login to PostgreSQL - sudo -u postgres psql -d template1 - # Create a user for GitLab - # Do not type the 'template1=#', this is part of the prompt - template1=# CREATE USER git CREATEDB; + sudo -u postgres psql -d template1 -c "CREATE USER git CREATEDB;" # Create the GitLab production database & grant all privileges on database - template1=# CREATE DATABASE gitlabhq_production OWNER git; - - # Quit the database session - template1=# \q + sudo -u postgres psql -d template1 -c "CREATE DATABASE gitlabhq_production OWNER git;" # Try connecting to the new database with the new user sudo -u git -H psql -d gitlabhq_production diff --git a/doc/install/requirements.md b/doc/install/requirements.md index d59b7f0e84d..03cb08dd1f1 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -22,7 +22,7 @@ For the installations options please see [the installation page on the GitLab we - FreeBSD On the above unsupported distributions is still possible to install GitLab yourself. -Please see the [installation from source guide](https://github.com/gitlabhq/gitlabhq/blob/master/doc/install/installation.md) and the [unofficial installation guides](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Unofficial-Installation-Guides) on the public wiki for more information. +Please see the [installation from source guide](installation.md) and the [installation guides](https://about.gitlab.com/installation/) for more information. ### Non-Unix operating systems such as Windows diff --git a/doc/integration/auth0.md b/doc/integration/auth0.md new file mode 100644 index 00000000000..e5247082a89 --- /dev/null +++ b/doc/integration/auth0.md @@ -0,0 +1,89 @@ +# Auth0 OmniAuth Provider + +To enable the Auth0 OmniAuth provider, you must create an Auth0 account, and an +application. + +1. Sign in to the [Auth0 Console](https://manage.auth0.com). If you need to +create an account, you can do so at the same link. + +1. Select "New App/API". + +1. Provide the Application Name ('GitLab' works fine). + +1. Once created, you should see the Quick Start options. Disregard them and +select 'Settings' above the Quick Start options. + +1. At the top of the Settings screen, you should see your Domain, Client ID and +Client Secret. Take note of these as you'll need to put them in the +configuration file. For example: + - Domain: `test1234.auth0.com` + - Client ID: `t6X8L2465bNePWLOvt9yi41i` + - Client Secret: `KbveM3nqfjwCbrhaUy_gDu2dss8TIlHIdzlyf33pB7dEK5u_NyQdp65O_o02hXs2` + +1. Fill in the Allowed Callback URLs: + - http://`YOUR_GITLAB_URL`/users/auth/auth0/callback (or) + - https://`YOUR_GITLAB_URL`/users/auth/auth0/callback + +1. Fill in the Allowed Origins (CORS): + - http://`YOUR_GITLAB_URL` (or) + - https://`YOUR_GITLAB_URL` + +1. On your GitLab server, open the configuration file. + + For omnibus package: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For installations from source: + + ```sh + cd /home/git/gitlab + sudo -u git -H editor config/gitlab.yml + ``` + +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) +for initial settings. + +1. Add the provider configuration: + + For omnibus package: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "auth0", + "args" => { client_id: 'YOUR_AUTH0_CLIENT_ID'', + client_secret: 'YOUR_AUTH0_CLIENT_SECRET', + namespace: 'YOUR_AUTH0_DOMAIN' + } + } + ] + ``` + + For installations from source: + + ```yaml + - { name: 'auth0', + args: { + client_id: 'YOUR_AUTH0_CLIENT_ID', + client_secret: 'YOUR_AUTH0_CLIENT_SECRET', + namespace: 'YOUR_AUTH0_DOMAIN' + } + } + ``` + +1. Change `YOUR_AUTH0_CLIENT_ID` to the client ID from the Auth0 Console page +from step 5. + +1. Change `YOUR_AUTH0_CLIENT_SECRET` to the client secret from the Auth0 Console +page from step 5. + +1. Save the file and [reconfigure GitLab](../administration/restart_gitlab.md) +for the changes to take effect. + +On the sign in page there should now be an Auth0 icon below the regular sign in +form. Click the icon to begin the authentication process. Auth0 will ask the +user to sign in and authorize the GitLab application. If everything goes well +the user will be returned to GitLab and will be signed in. diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index ba47cb16265..25f35988305 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -28,6 +28,7 @@ contains some settings that are common for all providers. - [SAML](saml.md) - [Crowd](crowd.md) - [Azure](azure.md) +- [Auth0](auth0.md) ## Initial OmniAuth Configuration diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index 2dfd08755ba..3d375e47c8e 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -74,17 +74,21 @@ Any user can remove themselves from a group, unless they are the last Owner of t ## External Users -In cases where it is desired that a user has access to some internal or private projects, but others -should remain hidden from this user, there is the option of creating `External Users`. -An administrator can flag a user as external through the API or by checking the checkbox on the admin panel. - -In the case of a new user: navigate to the **Admin** area and click the **New User** button. If you would like to -edit a user, go to the user list on the **Admin** area and click the **Edit** button. - -External users can only access projects to which they are explicitly granted access, thus hiding all internal projects. -Access can be granted by adding the users as member to the project or by including this user in a group. External users will, like usual users, receive -a role in the project or group with all the abilities that are mentioned in the table above. - -External users cannot create groups or projects, and have the same access as logged out users in all other cases. This feature may be -useful when for example a contractor is working on a given project and should only access the given project and public -projects. +In cases where it is desired that a user has access only to some internal or +private projects, there is the option of creating **External Users**. This +feature may be useful when for example a contractor is working on a given +project and should only have access to that project. + +External users can only access projects to which they are explicitly granted +access, thus hiding all other internal or private ones from them. Access can be +granted by adding the user as member to the project or group. + +They will, like usual users, receive a role in the project or group with all +the abilities that are mentioned in the table above. They cannot however create +groups or projects, and they have the same access as logged out users in all +other cases. + +An administrator can flag a user as external [through the API](../api/users.md) +or by checking the checkbox on the admin panel. As an administrator, navigate +to **Admin > Users** to create a new user or edit an existing one. There, you +will find the option to flag the user as external. diff --git a/doc/release/security.md b/doc/release/security.md index b1a62b333e6..118c016ba4f 100644 --- a/doc/release/security.md +++ b/doc/release/security.md @@ -15,7 +15,7 @@ Please report suspected security vulnerabilities in private to <support@gitlab.c 1. Verify that the issue can be reproduced 1. Acknowledge the issue to the researcher that disclosed it 1. Inform the release manager that there needs to be a security release -1. Do the steps from [patch release document](doc/release/patch.md), starting with "Create an issue on private GitLab development server" +1. Do the steps from [patch release document](../release/patch.md), starting with "Create an issue on private GitLab development server" 1. The MR with the security fix should get a 'security' label and be assigned to the release manager 1. Build the package for GitLab.com and do a deploy 1. Build the package for ci.gitLab.com and do a deploy diff --git a/doc/security/two_factor_authentication.md b/doc/security/two_factor_authentication.md index 8365bdb7b1b..c8499380c18 100644 --- a/doc/security/two_factor_authentication.md +++ b/doc/security/two_factor_authentication.md @@ -6,7 +6,7 @@ password to login, they'll be prompted for a code generated by an application on their phone. You can read more about it here: -[Two-factor Authentication (2FA)](doc/profile/two_factor_authentication.md) +[Two-factor Authentication (2FA)](../profile/two_factor_authentication.md) ## Enabling 2FA diff --git a/doc/update/8.2-to-8.3.md b/doc/update/8.2-to-8.3.md index 2ca4e1f3770..9f5c6c4dc84 100644 --- a/doc/update/8.2-to-8.3.md +++ b/doc/update/8.2-to-8.3.md @@ -1,5 +1,14 @@ # From 8.2 to 8.3 +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + **NOTE:** GitLab 8.0 introduced several significant changes related to installation and configuration which *are not duplicated here*. Be sure you're already running a working version of at least 8.0 before proceeding with this diff --git a/doc/update/8.3-to-8.4.md b/doc/update/8.3-to-8.4.md index 269deec7a9c..9f6517d9487 100644 --- a/doc/update/8.3-to-8.4.md +++ b/doc/update/8.3-to-8.4.md @@ -1,5 +1,14 @@ # From 8.3 to 8.4 +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + ### 1. Stop server sudo service gitlab stop diff --git a/doc/update/8.4-to-8.5.md b/doc/update/8.4-to-8.5.md index 0a9cb5683e7..0cb137a03cc 100644 --- a/doc/update/8.4-to-8.5.md +++ b/doc/update/8.4-to-8.5.md @@ -1,5 +1,14 @@ # From 8.4 to 8.5 +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + ### 1. Stop server sudo service gitlab stop diff --git a/doc/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md index 024f6e8a433..7d63915af5e 100644 --- a/doc/update/8.5-to-8.6.md +++ b/doc/update/8.5-to-8.6.md @@ -1,5 +1,14 @@ # From 8.5 to 8.6 +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + ### 1. Stop server sudo service gitlab stop diff --git a/doc/workflow/importing/import_projects_from_bitbucket.md b/doc/workflow/importing/import_projects_from_bitbucket.md index 1e9825e2e10..520c4216295 100644 --- a/doc/workflow/importing/import_projects_from_bitbucket.md +++ b/doc/workflow/importing/import_projects_from_bitbucket.md @@ -1,6 +1,6 @@ # Import your project from Bitbucket to GitLab
-It takes just a few steps to import your existing Bitbucket projects to GitLab. But keep in mind that it is possible only if Bitbucket support is enabled on your GitLab instance. You can read more about Bitbucket support [here](doc/integration/bitbucket.md).
+It takes just a few steps to import your existing Bitbucket projects to GitLab. But keep in mind that it is possible only if Bitbucket support is enabled on your GitLab instance. You can read more about Bitbucket support [here](../../integration/bitbucket.md).
* Sign in to GitLab.com and go to your dashboard
diff --git a/doc/workflow/protected_branches.md b/doc/workflow/protected_branches.md index fdf9a8d391c..d854ec1e025 100644 --- a/doc/workflow/protected_branches.md +++ b/doc/workflow/protected_branches.md @@ -12,7 +12,7 @@ A protected branch does three simple things: You can make any branch a protected branch. GitLab makes the master branch a protected branch by default. -To protect a branch, user needs to have at least a Master permission level, see [permissions document](doc/permissions/permissions.md). +To protect a branch, user needs to have at least a Master permission level, see [permissions document](../permissions/permissions.md). ![protected branches page](protected_branches/protected_branches1.png) diff --git a/features/project/issues/award_emoji.feature b/features/project/issues/award_emoji.feature index 2945bb3753a..f0fd414a9f9 100644 --- a/features/project/issues/award_emoji.feature +++ b/features/project/issues/award_emoji.feature @@ -18,21 +18,24 @@ Feature: Award Emoji @javascript Scenario: I add and remove custom award in the issue Given I click to emoji-picker - Then The search field is focused - And I click to emoji in the picker + Then The emoji menu is visible + And The search field is focused + Then I click to emoji in the picker Then I have award added And I can remove it by clicking to icon @javascript Scenario: I can see the list of emoji categories Given I click to emoji-picker - Then The search field is focused + Then The emoji menu is visible + And The search field is focused Then I can see the activity and food categories @javascript Scenario: I can search emoji Given I click to emoji-picker - Then The search field is focused + Then The emoji menu is visible + And The search field is focused And I search "hand" Then I see search result for "hand" diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature index 74685d24a7d..823658b4f24 100644 --- a/features/project/merge_requests.feature +++ b/features/project/merge_requests.feature @@ -325,3 +325,11 @@ Feature: Project Merge Requests When I click the "Target branch" dropdown And I select a new target branch Then I should see new target branch changes + + @javascript + Scenario: I can close merge request after commenting + Given I visit merge request page "Bug NS-04" + And I leave a comment like "XML attached" + Then I should see comment "XML attached" + And I click link "Close" + Then I should see closed merge request "Bug NS-04" diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb index 9722a5a848c..963e4f21365 100644 --- a/features/steps/dashboard/todos.rb +++ b/features/steps/dashboard/todos.rb @@ -41,7 +41,6 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps click_link 'Done' end - expect(page).to have_content 'Todo was successfully marked as done.' expect(page).to have_content 'To do 3' expect(page).to have_content 'Done 1' should_not_see_todo "John Doe assigned you merge request !#{merge_request.iid}" diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 7a6ae15ffa5..e5b7db4c5e3 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -35,7 +35,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end step 'I should see projects activity feed' do - expect(page).to have_content 'closed issue' + expect(page).to have_content 'joined project' end step 'I should see issues from group "Owned" assigned to me' do diff --git a/features/steps/project/badges/build.rb b/features/steps/project/badges/build.rb index 47540f356e9..66a48a176e5 100644 --- a/features/steps/project/badges/build.rb +++ b/features/steps/project/badges/build.rb @@ -21,7 +21,7 @@ class Spinach::Features::ProjectBadgesBuild < Spinach::FeatureSteps end step 'I should see a badge that has not been cached' do - expect(page.response_headers).to include('Cache-Control' => 'no-cache') + expect(page.response_headers['Cache-Control']).to include 'no-cache' end def expect_badge(status) diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb index ce2554bc80d..c5d45709b44 100644 --- a/features/steps/project/issues/award_emoji.rb +++ b/features/steps/project/issues/award_emoji.rb @@ -92,6 +92,10 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps end end + step 'The emoji menu is visible' do + page.find(".emoji-menu.is-visible") + end + step 'The search field is focused' do expect(page).to have_selector('#emoji_search') expect(page.evaluate_script('document.activeElement.id')).to eq('emoji_search') diff --git a/features/steps/search.rb b/features/steps/search.rb index 48ea3fa3876..0ad837ebe1d 100644 --- a/features/steps/search.rb +++ b/features/steps/search.rb @@ -100,7 +100,7 @@ class Spinach::Features::Search < Spinach::FeatureSteps step 'I should see "test_wiki" link in the search results' do page.within('.results') do - find(:css, '.search-results').should have_link 'test_wiki.md' + expect(find(:css, '.search-results')).to have_link 'test_wiki' end end diff --git a/features/support/capybara.rb b/features/support/capybara.rb index f33379f76c9..fe9e39cf509 100644 --- a/features/support/capybara.rb +++ b/features/support/capybara.rb @@ -9,7 +9,7 @@ Capybara.register_driver :poltergeist do |app| Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout, window_size: [1366, 768]) end -Capybara.default_wait_time = timeout +Capybara.default_max_wait_time = timeout Capybara.ignore_hidden_elements = false unless ENV['CI'] || ENV['CI_SERVER'] diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 252744515da..fda6f841438 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -82,7 +82,7 @@ module API # GET /projects/:id/issues?milestone=1.0.0&state=closed # GET /issues?iid=42 get ":id/issues" do - issues = user_project.issues + issues = user_project.issues.visible_to_user(current_user) issues = filter_issues_state(issues, params[:state]) unless params[:state].nil? issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil? issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil? @@ -104,6 +104,7 @@ module API # GET /projects/:id/issues/:issue_id get ":id/issues/:issue_id" do @issue = user_project.issues.find(params[:issue_id]) + not_found! unless can?(current_user, :read_issue, @issue) present @issue, with: Entities::Issue end diff --git a/lib/api/users.rb b/lib/api/users.rb index c574f042a66..13ab17c6904 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -61,7 +61,7 @@ module API # admin - User is admin - true or false (default) # can_create_group - User can create groups - true or false # confirm - Require user confirmation - true (default) or false - # external - Is user an external user - true or false(default) + # external - Flags the user as external - true or false(default) # Example Request: # POST /users post do @@ -108,12 +108,13 @@ module API # bio - Bio # admin - User is admin - true or false (default) # can_create_group - User can create groups - true or false + # external - Flags the user as external - true or false(default) # Example Request: # PUT /users/:id put ":id" do authenticated_as_admin! - attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :can_create_group, :admin] + attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :can_create_group, :admin, :external] user = User.find(params[:id]) not_found!('User') unless user diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb index 9f08aa36e8b..2732e0b5145 100644 --- a/lib/banzai/filter/issue_reference_filter.rb +++ b/lib/banzai/filter/issue_reference_filter.rb @@ -9,6 +9,11 @@ module Banzai Issue end + def self.user_can_see_reference?(user, node, context) + issue = Issue.find(node.attr('data-issue')) rescue nil + Ability.abilities.allowed?(user, :read_issue, issue) + end + def find_object(project, id) project.get_issue(id) end diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index faa2830c16e..d2e85cabf72 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -24,6 +24,10 @@ module Gitlab @lines ||= parser.parse(raw_diff.each_line).to_a end + def too_large? + diff.too_large? + end + def highlighted_diff_lines Gitlab::Diff::Highlight.new(self).highlight end diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb index d0f6ba23ab4..d0815fc7eea 100644 --- a/lib/gitlab/diff/parser.rb +++ b/lib/gitlab/diff/parser.rb @@ -4,6 +4,8 @@ module Gitlab include Enumerable def parse(lines) + return [] if lines.blank? + @lines = lines line_obj_index = 0 line_old = 1 diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb new file mode 100644 index 00000000000..a088e19d1e7 --- /dev/null +++ b/lib/gitlab/git_post_receive.rb @@ -0,0 +1,60 @@ +module Gitlab + class GitPostReceive + include Gitlab::Identifier + attr_reader :repo_path, :identifier, :changes, :project + + def initialize(repo_path, identifier, changes) + repo_path.gsub!(/\.git\z/, '') + repo_path.gsub!(/\A\//, '') + + @repo_path = repo_path + @identifier = identifier + @changes = deserialize_changes(changes) + + retrieve_project_and_type + end + + def wiki? + @type == :wiki + end + + def regular_project? + @type == :project + end + + def identify(revision) + super(identifier, project, revision) + end + + private + + def retrieve_project_and_type + @type = :project + @project = Project.find_with_namespace(@repo_path) + + if @repo_path.end_with?('.wiki') && !@project + @type = :wiki + @project = Project.find_with_namespace(@repo_path.gsub(/\.wiki\z/, '')) + end + end + + def deserialize_changes(changes) + changes = Base64.decode64(changes) unless changes.include?(' ') + changes = utf8_encode_changes(changes) + changes.lines + 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 + end +end diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 0607a8b9592..71c5b6801fb 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -2,7 +2,8 @@ module Gitlab class ProjectSearchResults < SearchResults attr_reader :project, :repository_ref - def initialize(project, query, repository_ref = nil) + def initialize(current_user, project, query, repository_ref = nil) + @current_user = current_user @project = project @repository_ref = if repository_ref.present? repository_ref diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index f13528a2eea..f8ab2b1f09e 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -1,12 +1,13 @@ module Gitlab class SearchResults - attr_reader :query + attr_reader :current_user, :query # Limit search results by passed projects # It allows us to search only for projects user has access to attr_reader :limit_projects - def initialize(limit_projects, query) + def initialize(current_user, limit_projects, query) + @current_user = current_user @limit_projects = limit_projects || Project.all @query = Shellwords.shellescape(query) if query.present? end @@ -58,7 +59,7 @@ module Gitlab end def issues - issues = Issue.where(project_id: project_ids_relation) + issues = Issue.visible_to_user(current_user).where(project_id: project_ids_relation) if query =~ /#(\d+)\z/ issues = issues.where(iid: $1) diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index fc5475c4eef..1324e4cd267 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -30,7 +30,6 @@ server { listen [::]:80 default_server; server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com server_tokens off; ## Don't show the nginx version number, a security best practice - root /home/git/gitlab/public; ## See app/controllers/application_controller.rb for headers set @@ -57,4 +56,14 @@ server { proxy_pass http://gitlab-workhorse; } + + error_page 404 /404.html; + error_page 422 /422.html; + error_page 500 /500.html; + error_page 502 /502.html; + location ~ ^/(404|422|500|502)\.html$ { + root /home/git/gitlab/public; + internal; + } + } diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 1e5f85413ec..af6ea9ed706 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -45,7 +45,6 @@ server { listen [::]:443 ipv6only=on ssl default_server; server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com server_tokens off; ## Don't show the nginx version number, a security best practice - root /home/git/gitlab/public; ## Strong SSL Security ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/ @@ -101,4 +100,13 @@ server { proxy_set_header X-Forwarded-Proto $scheme; proxy_pass http://gitlab-workhorse; } + + error_page 404 /404.html; + error_page 422 /422.html; + error_page 500 /500.html; + error_page 502 /502.html; + location ~ ^/(404|422|500|502)\.html$ { + root /home/git/gitlab/public; + internal; + } } diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake index ebe301c1fc7..cfaf4a129b1 100644 --- a/lib/tasks/gemojione.rake +++ b/lib/tasks/gemojione.rake @@ -47,6 +47,7 @@ namespace :gemojione do # let's simplify it system(%Q(sed -i '' "s/width: #{SIZE}px; height: #{SIZE}px; background: image-url('emoji.png')/background-position:/" #{style_path})) system(%Q(sed -i '' "s/ no-repeat//" #{style_path})) + system(%Q(sed -i '' "s/ 0px/ 0/" #{style_path})) # Append a generic rule that applies to all Emojis File.open(style_path, 'a') do |f| diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 581ab26db79..27ed57efe55 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -913,7 +913,7 @@ namespace :gitlab do end def check_git_version - required_version = Gitlab::VersionInfo.new(1, 7, 10) + required_version = Gitlab::VersionInfo.new(2, 7, 3) current_version = Gitlab::VersionInfo.parse(run(%W(#{Gitlab.config.git.bin_path} --version))) puts "Your git bin path is \"#{Gitlab.config.git.bin_path}\"" diff --git a/public/404.html b/public/404.html index a0106bc760d..4862770cc2a 100644 --- a/public/404.html +++ b/public/404.html @@ -2,11 +2,51 @@ <html> <head> <title>The page you're looking for could not be found (404)</title> - <link href="/static.css" media="screen" rel="stylesheet" type="text/css" /> + <style> + body { + color: #666; + text-align: center; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + margin: 0; + width: 800px; + margin: auto; + font-size: 14px; + } + + h1 { + font-size: 56px; + line-height: 100px; + font-weight: normal; + color: #456; + } + + h2 { + font-size: 24px; + color: #666; + line-height: 1.5em; + } + + h3 { + color: #456; + font-size: 20px; + font-weight: normal; + line-height: 28px; + } + + hr { + margin: 18px 0; + border: 0; + border-top: 1px solid #EEE; + border-bottom: 1px solid white; + } + </style> </head> <body> - <h1>404</h1> + <h1> + <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br /> + 404 + </h1> <h3>The page you're looking for could not be found.</h3> <hr/> <p>Make sure the address is correct and that the page hasn't moved.</p> diff --git a/public/422.html b/public/422.html index 026997b48e3..055b0bde165 100644 --- a/public/422.html +++ b/public/422.html @@ -2,12 +2,51 @@ <html> <head> <title>The change you requested was rejected (422)</title> - <link href="/static.css" media="screen" rel="stylesheet" type="text/css" /> + <style> + body { + color: #666; + text-align: center; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + margin: 0; + width: 800px; + margin: auto; + font-size: 14px; + } + + h1 { + font-size: 56px; + line-height: 100px; + font-weight: normal; + color: #456; + } + + h2 { + font-size: 24px; + color: #666; + line-height: 1.5em; + } + + h3 { + color: #456; + font-size: 20px; + font-weight: normal; + line-height: 28px; + } + + hr { + margin: 18px 0; + border: 0; + border-top: 1px solid #EEE; + border-bottom: 1px solid white; + } + </style> </head> <body> - <!-- This file lives in public/422.html --> - <h1>422</h1> + <h1> + <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br /> + 422 + </h1> <h3>The change you requested was rejected.</h3> <hr /> <p>Make sure you have access to the thing you tried to change.</p> diff --git a/public/500.html b/public/500.html index 08c11bbd05a..3d59d1392f5 100644 --- a/public/500.html +++ b/public/500.html @@ -2,10 +2,50 @@ <html> <head> <title>Something went wrong (500)</title> - <link href="/static.css" media="screen" rel="stylesheet" type="text/css" /> + <style> + body { + color: #666; + text-align: center; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + margin: 0; + width: 800px; + margin: auto; + font-size: 14px; + } + + h1 { + font-size: 56px; + line-height: 100px; + font-weight: normal; + color: #456; + } + + h2 { + font-size: 24px; + color: #666; + line-height: 1.5em; + } + + h3 { + color: #456; + font-size: 20px; + font-weight: normal; + line-height: 28px; + } + + hr { + margin: 18px 0; + border: 0; + border-top: 1px solid #EEE; + border-bottom: 1px solid white; + } + </style> </head> <body> - <h1>500</h1> + <h1> + <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br /> + 500 + </h1> <h3>Whoops, something went wrong on our end.</h3> <hr/> <p>Try refreshing the page, or going back and attempting the action again.</p> diff --git a/public/502.html b/public/502.html index 9480a928439..67dfd8a2743 100644 --- a/public/502.html +++ b/public/502.html @@ -2,10 +2,50 @@ <html> <head> <title>GitLab is not responding (502)</title> - <link href="/static.css" media="screen" rel="stylesheet" type="text/css" /> + <style> + body { + color: #666; + text-align: center; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + margin: 0; + width: 800px; + margin: auto; + font-size: 14px; + } + + h1 { + font-size: 56px; + line-height: 100px; + font-weight: normal; + color: #456; + } + + h2 { + font-size: 24px; + color: #666; + line-height: 1.5em; + } + + h3 { + color: #456; + font-size: 20px; + font-weight: normal; + line-height: 28px; + } + + hr { + margin: 18px 0; + border: 0; + border-top: 1px solid #EEE; + border-bottom: 1px solid white; + } + </style> </head> <body> - <h1>502</h1> + <h1> + <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br /> + 502 + </h1> <h3>Whoops, GitLab is taking too much time to respond.</h3> <hr/> <p>Try refreshing the page, or going back and attempting the action again.</p> diff --git a/public/deploy.html b/public/deploy.html index 3822ed4b64d..48976dacf41 100644 --- a/public/deploy.html +++ b/public/deploy.html @@ -2,12 +2,49 @@ <html> <head> <title>Deploy in progress</title> - <link href="/static.css" media="screen" rel="stylesheet" type="text/css" /> + <style> + body { + color: #666; + text-align: center; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + margin: 0; + width: 800px; + margin: auto; + font-size: 14px; + } + + h1 { + font-size: 56px; + line-height: 100px; + font-weight: normal; + color: #456; + } + + h2 { + font-size: 24px; + color: #666; + line-height: 1.5em; + } + + h3 { + color: #456; + font-size: 20px; + font-weight: normal; + line-height: 28px; + } + + hr { + margin: 18px 0; + border: 0; + border-top: 1px solid #EEE; + border-bottom: 1px solid white; + } + </style> </head> <body> <h1> - <img src="/logo.svg" /><br /> + <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br /> Deploy in progress </h1> <h3>Please try again in a few minutes.</h3> diff --git a/public/logo.svg b/public/logo.svg deleted file mode 100644 index fc4553137f7..00000000000 --- a/public/logo.svg +++ /dev/null @@ -1,9 +0,0 @@ -<svg width="210" height="210" viewBox="0 0 210 210" xmlns="http://www.w3.org/2000/svg"> - <path d="M105.0614 203.655l38.64-118.921h-77.28l38.64 118.921z" fill="#e24329"/> - <path d="M105.0614 203.6548l-38.64-118.921h-54.153l92.793 118.921z" fill="#fc6d26"/> - <path d="M12.2685 84.7341l-11.742 36.139c-1.071 3.296.102 6.907 2.906 8.944l101.629 73.838-92.793-118.921z" fill="#fca326"/> - <path d="M12.2685 84.7342h54.153l-23.273-71.625c-1.197-3.686-6.411-3.685-7.608 0l-23.272 71.625z" fill="#e24329"/> - <path d="M105.0614 203.6548l38.64-118.921h54.153l-92.793 118.921z" fill="#fc6d26"/> - <path d="M197.8544 84.7341l11.742 36.139c1.071 3.296-.102 6.907-2.906 8.944l-101.629 73.838 92.793-118.921z" fill="#fca326"/> - <path d="M197.8544 84.7342h-54.153l23.273-71.625c1.197-3.686 6.411-3.685 7.608 0l23.272 71.625z" fill="#e24329"/> -</svg> diff --git a/public/static.css b/public/static.css deleted file mode 100644 index 0a2b6060d48..00000000000 --- a/public/static.css +++ /dev/null @@ -1,36 +0,0 @@ -body { - color: #666; - text-align: center; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - margin: 0; - width: 800px; - margin: auto; - font-size: 14px; -} - -h1 { - font-size: 56px; - line-height: 100px; - font-weight: normal; - color: #456; -} - -h2 { - font-size: 24px; - color: #666; - line-height: 1.5em; -} - -h3 { - color: #456; - font-size: 20px; - font-weight: normal; - line-height: 28px; -} - -hr { - margin: 18px 0; - border: 0; - border-top: 1px solid #EEE; - border-bottom: 1px solid white; -} diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index 8e06d4bdc77..98ae424ed7c 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -17,49 +17,79 @@ describe Projects::BranchesController do describe "POST create" do render_views - before do - post :create, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - branch_name: branch, - ref: ref - end + context "on creation of a new branch" do + before do + post :create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + branch_name: branch, + ref: ref + end - context "valid branch name, valid source" do - let(:branch) { "merge_branch" } - let(:ref) { "master" } - it 'redirects' do - expect(subject). - to redirect_to("/#{project.path_with_namespace}/tree/merge_branch") + context "valid branch name, valid source" do + let(:branch) { "merge_branch" } + let(:ref) { "master" } + it 'redirects' do + expect(subject). + to redirect_to("/#{project.path_with_namespace}/tree/merge_branch") + end + end + + context "invalid branch name, valid ref" do + let(:branch) { "<script>alert('merge');</script>" } + let(:ref) { "master" } + it 'redirects' do + expect(subject). + to redirect_to("/#{project.path_with_namespace}/tree/alert('merge');") + end + end + + context "valid branch name, invalid ref" do + let(:branch) { "merge_branch" } + let(:ref) { "<script>alert('ref');</script>" } + it { is_expected.to render_template('new') } + end + + context "invalid branch name, invalid ref" do + let(:branch) { "<script>alert('merge');</script>" } + let(:ref) { "<script>alert('ref');</script>" } + it { is_expected.to render_template('new') } + end + + context "valid branch name with encoded slashes" do + let(:branch) { "feature%2Ftest" } + let(:ref) { "<script>alert('ref');</script>" } + it { is_expected.to render_template('new') } + it { project.repository.branch_names.include?('feature/test') } end end - context "invalid branch name, valid ref" do - let(:branch) { "<script>alert('merge');</script>" } - let(:ref) { "master" } + describe "created from the new branch button on issues" do + let(:branch) { "1-feature-branch" } + let!(:issue) { create(:issue, project: project) } + + it 'redirects' do + post :create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + branch_name: branch, + issue_iid: issue.iid + expect(subject). - to redirect_to("/#{project.path_with_namespace}/tree/alert('merge');") + to redirect_to("/#{project.path_with_namespace}/tree/1-feature-branch") end - end - context "valid branch name, invalid ref" do - let(:branch) { "merge_branch" } - let(:ref) { "<script>alert('ref');</script>" } - it { is_expected.to render_template('new') } - end + it 'posts a system note' do + expect(SystemNoteService).to receive(:new_issue_branch).with(issue, project, user, "1-feature-branch") - context "invalid branch name, invalid ref" do - let(:branch) { "<script>alert('merge');</script>" } - let(:ref) { "<script>alert('ref');</script>" } - it { is_expected.to render_template('new') } - end + post :create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + branch_name: branch, + issue_iid: issue.iid + end - context "valid branch name with encoded slashes" do - let(:branch) { "feature%2Ftest" } - let(:ref) { "<script>alert('ref');</script>" } - it { is_expected.to render_template('new') } - it { project.repository.branch_names.include?('feature/test')} end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 76d56bc989d..2cd81231144 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -1,16 +1,16 @@ require('spec_helper') describe Projects::IssuesController do - let(:project) { create(:project) } - let(:user) { create(:user) } - let(:issue) { create(:issue, project: project) } + describe "GET #index" do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:issue) { create(:issue, project: project) } - before do - sign_in(user) - project.team << [user, :developer] - end + before do + sign_in(user) + project.team << [user, :developer] + end - describe "GET #index" do it "returns index" do get :index, namespace_id: project.namespace.path, project_id: project.path @@ -38,6 +38,152 @@ describe Projects::IssuesController do get :index, namespace_id: project.namespace.path, project_id: project.path expect(response.status).to eq(404) end + end + + describe 'Confidential Issues' do + let(:project) { create(:empty_project, :public) } + let(:assignee) { create(:assignee) } + let(:author) { create(:user) } + let(:non_member) { create(:user) } + let(:member) { create(:user) } + let(:admin) { create(:admin) } + let!(:issue) { create(:issue, project: project) } + let!(:unescaped_parameter_value) { create(:issue, :confidential, project: project, author: author) } + let!(:request_forgery_timing_attack) { create(:issue, :confidential, project: project, assignee: assignee) } + + describe 'GET #index' do + it 'should not list confidential issues for guests' do + sign_out(:user) + get_issues + + expect(assigns(:issues)).to eq [issue] + end + + it 'should not list confidential issues for non project members' do + sign_in(non_member) + get_issues + + expect(assigns(:issues)).to eq [issue] + end + + it 'should list confidential issues for author' do + sign_in(author) + get_issues + + expect(assigns(:issues)).to include unescaped_parameter_value + expect(assigns(:issues)).not_to include request_forgery_timing_attack + end + + it 'should list confidential issues for assignee' do + sign_in(assignee) + get_issues + + expect(assigns(:issues)).not_to include unescaped_parameter_value + expect(assigns(:issues)).to include request_forgery_timing_attack + end + + it 'should list confidential issues for project members' do + sign_in(member) + project.team << [member, :developer] + + get_issues + + expect(assigns(:issues)).to include unescaped_parameter_value + expect(assigns(:issues)).to include request_forgery_timing_attack + end + + it 'should list confidential issues for admin' do + sign_in(admin) + get_issues + + expect(assigns(:issues)).to include unescaped_parameter_value + expect(assigns(:issues)).to include request_forgery_timing_attack + end + + def get_issues + get :index, + namespace_id: project.namespace.to_param, + project_id: project.to_param + end + end + shared_examples_for 'restricted action' do |http_status| + it 'returns 404 for guests' do + sign_out :user + go(id: unescaped_parameter_value.to_param) + + expect(response).to have_http_status :not_found + end + + it 'returns 404 for non project members' do + sign_in(non_member) + go(id: unescaped_parameter_value.to_param) + + expect(response).to have_http_status :not_found + end + + it "returns #{http_status[:success]} for author" do + sign_in(author) + go(id: unescaped_parameter_value.to_param) + + expect(response).to have_http_status http_status[:success] + end + + it "returns #{http_status[:success]} for assignee" do + sign_in(assignee) + go(id: request_forgery_timing_attack.to_param) + + expect(response).to have_http_status http_status[:success] + end + + it "returns #{http_status[:success]} for project members" do + sign_in(member) + project.team << [member, :developer] + go(id: unescaped_parameter_value.to_param) + + expect(response).to have_http_status http_status[:success] + end + + it "returns #{http_status[:success]} for admin" do + sign_in(admin) + go(id: unescaped_parameter_value.to_param) + + expect(response).to have_http_status http_status[:success] + end + end + + describe 'GET #show' do + it_behaves_like 'restricted action', success: 200 + + def go(id:) + get :show, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: id + end + end + + describe 'GET #edit' do + it_behaves_like 'restricted action', success: 200 + + def go(id:) + get :edit, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: id + end + end + + describe 'PUT #update' do + it_behaves_like 'restricted action', success: 302 + + def go(id:) + put :update, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: id, + issue: { title: 'New title' } + end + end end end diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb index 722095de590..e72aa9479b7 100644 --- a/spec/factories/issues.rb +++ b/spec/factories/issues.rb @@ -4,6 +4,10 @@ FactoryGirl.define do author project + trait :confidential do + confidential true + end + trait :closed do state :closed end diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index a9df5fa1d3a..e281e2f227b 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -51,6 +51,11 @@ FactoryGirl.define do trait :with_diffs do end + trait :without_diffs do + source_branch "improve/awesome" + target_branch "master" + end + trait :conflict do source_branch "feature_conflict" target_branch "feature" diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb index bd85b1d798a..7ae06c27840 100644 --- a/spec/factories/todos.rb +++ b/spec/factories/todos.rb @@ -5,14 +5,15 @@ # id :integer not null, primary key # user_id :integer not null # project_id :integer not null -# target_id :integer not null +# target_id :integer # target_type :string not null # author_id :integer -# note_id :integer # action :integer not null # state :string not null # created_at :datetime # updated_at :datetime +# note_id :integer +# commit_id :string # FactoryGirl.define do @@ -30,5 +31,10 @@ FactoryGirl.define do trait :mentioned do action { Todo::MENTIONED } end + + trait :on_commit do + commit_id RepoHelpers.sample_commit.id + target_type "Commit" + end end end diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb new file mode 100644 index 00000000000..9219b767547 --- /dev/null +++ b/spec/features/issues/new_branch_button_spec.rb @@ -0,0 +1,49 @@ +require 'rails_helper' + +feature 'Start new branch from an issue', feature: true do + let!(:project) { create(:project) } + let!(:issue) { create(:issue, project: project) } + let!(:user) { create(:user)} + + context "for team members" do + before do + project.team << [user, :master] + login_as(user) + end + + it 'shown the new branch button', js: false do + visit namespace_project_issue_path(project.namespace, project, issue) + + expect(page).to have_link "New Branch" + end + + context "when there is a referenced merge request" do + let(:note) do + create(:note, :on_issue, :system, project: project, + note: "mentioned in !#{referenced_mr.iid}") + end + let(:referenced_mr) do + create(:merge_request, :simple, source_project: project, target_project: project, + description: "Fixes ##{issue.iid}", author: user) + end + + before do + issue.notes << note + + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it "hides the new branch button", js: true do + expect(page).not_to have_link "New Branch" + expect(page).to have_content /1 Related Merge Request/ + end + end + end + + context "for visiters" do + it 'no button is shown', js: false do + visit namespace_project_issue_path(project.namespace, project, issue) + expect(page).not_to have_link "New Branch" + end + end +end diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb index 1b2fd1bab10..b76e4c74c79 100644 --- a/spec/features/merge_requests/filter_by_milestone_spec.rb +++ b/spec/features/merge_requests/filter_by_milestone_spec.rb @@ -30,8 +30,6 @@ feature 'Merge Request filtering by Milestone', feature: true do def filter_by_milestone(title) find(".js-milestone-select").click - sleep 0.5 find(".milestone-filter a", text: title).click - sleep 1 end end diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index 4f129eca183..eca8bc8ab2d 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -11,7 +11,7 @@ describe LabelsHelper do end it 'uses the instance variable' do - expect(link_to_label(label)).to match %r{<a href="/#{@project.to_reference}/issues\?label_name=#{label.name}">.*</a>} + expect(link_to_label(label)).to match %r{<a href="/#{@project.to_reference}/issues\?label_name=#{label.name}"><span class="[\w\s\-]*has_tooltip".*</span></a>} end end @@ -39,6 +39,14 @@ describe LabelsHelper do end end + context 'with a tooltip argument' do + context 'set to false' do + it 'does not include the has_tooltip class' do + expect(link_to_label(label, tooltip: false)).not_to match %r{has_tooltip} + end + end + end + context 'with block' do it 'passes the block to link_to' do link = link_to_label(label) { 'Foo' } @@ -49,7 +57,7 @@ describe LabelsHelper do context 'without block' do it 'uses render_colored_label as the link content' do expect(self).to receive(:render_colored_label). - with(label).and_return('Foo') + with(label, tooltip: true).and_return('Foo') expect(link_to_label(label)).to match('Foo') end end diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index e2d21f53b7e..4c1d4a2d24c 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -56,7 +56,7 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do describe 'label span element' do it 'includes default classes' do doc = reference_filter("Label #{reference}") - expect(doc.css('a span').first.attr('class')).to eq 'label color-label' + expect(doc.css('a span').first.attr('class')).to eq 'label color-label has_tooltip' end it 'includes a style attribute' do diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb index e9bb388e361..9acf6304bcb 100644 --- a/spec/lib/banzai/filter/redactor_filter_spec.rb +++ b/spec/lib/banzai/filter/redactor_filter_spec.rb @@ -44,8 +44,78 @@ describe Banzai::Filter::RedactorFilter, lib: true do end end - context "for user references" do + context 'with data-issue' do + context 'for confidential issues' do + it 'removes references for non project members' do + non_member = create(:user) + project = create(:empty_project, :public) + issue = create(:issue, :confidential, project: project) + + link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') + doc = filter(link, current_user: non_member) + + expect(doc.css('a').length).to eq 0 + end + + it 'allows references for author' do + author = create(:user) + project = create(:empty_project, :public) + issue = create(:issue, :confidential, project: project, author: author) + + link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') + doc = filter(link, current_user: author) + + expect(doc.css('a').length).to eq 1 + end + + it 'allows references for assignee' do + assignee = create(:user) + project = create(:empty_project, :public) + issue = create(:issue, :confidential, project: project, assignee: assignee) + + link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') + doc = filter(link, current_user: assignee) + expect(doc.css('a').length).to eq 1 + end + + it 'allows references for project members' do + member = create(:user) + project = create(:empty_project, :public) + project.team << [member, :developer] + issue = create(:issue, :confidential, project: project) + + link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') + doc = filter(link, current_user: member) + + expect(doc.css('a').length).to eq 1 + end + + it 'allows references for admin' do + admin = create(:admin) + project = create(:empty_project, :public) + issue = create(:issue, :confidential, project: project) + + link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') + doc = filter(link, current_user: admin) + + expect(doc.css('a').length).to eq 1 + end + end + + it 'allows references for non confidential issues' do + user = create(:user) + project = create(:empty_project, :public) + issue = create(:issue, project: project) + + link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') + doc = filter(link, current_user: user) + + expect(doc.css('a').length).to eq 1 + end + end + + context "for user references" do context 'with data-group' do it 'removes unpermitted Group references' do user = create(:user) diff --git a/spec/lib/ci/status_spec.rb b/spec/lib/ci/status_spec.rb index 1539720bb8d..47f3df6e3ce 100644 --- a/spec/lib/ci/status_spec.rb +++ b/spec/lib/ci/status_spec.rb @@ -48,6 +48,29 @@ describe Ci::Status do it { is_expected.to eq 'success' } end + context 'success and canceled' do + let(:statuses) do + [create(type, status: :success), create(type, status: :canceled)] + end + it { is_expected.to eq 'failed' } + end + + context 'all canceled' do + let(:statuses) do + [create(type, status: :canceled), create(type, status: :canceled)] + end + it { is_expected.to eq 'canceled' } + end + + context 'success and canceled but allowed to fail' do + let(:statuses) do + [create(type, status: :success), + create(type, status: :canceled, allow_failure: true)] + end + + it { is_expected.to eq 'success' } + end + context 'one finished and second running but allowed to fail' do let(:statuses) do [create(type, status: :success), diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 04cf11fc6f1..844fd79c991 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -11,6 +11,7 @@ describe Gitlab::ClosingIssueExtractor, lib: true do subject { described_class.new(project, project.creator) } before do + project.team << [project.creator, :developer] project2.team << [project.creator, :master] end diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index 0d9694f2c13..a0cbef6e6a4 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -18,4 +18,18 @@ describe Gitlab::Diff::File, lib: true do describe :mode_changed? do it { expect(diff_file.mode_changed?).to be_falsey } end + + describe '#too_large?' do + it 'returns true for a file that is too large' do + expect(diff).to receive(:too_large?).and_return(true) + + expect(diff_file.too_large?).to eq(true) + end + + it 'returns false for a file that is small enough' do + expect(diff).to receive(:too_large?).and_return(false) + + expect(diff_file.too_large?).to eq(false) + end + end end diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb index f576c39284e..cdff063a9ed 100644 --- a/spec/lib/gitlab/diff/parser_spec.rb +++ b/spec/lib/gitlab/diff/parser_spec.rb @@ -90,4 +90,9 @@ eos end end end + + context 'when lines is empty' do + it { expect(parser.parse([])).to eq([]) } + it { expect(parser.parse(nil)).to eq([]) } + end end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 09adbc07dcb..db0ff95b4f5 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -1,11 +1,12 @@ require 'spec_helper' describe Gitlab::ProjectSearchResults, lib: true do + let(:user) { create(:user) } let(:project) { create(:project) } let(:query) { 'hello world' } describe 'initialize with empty ref' do - let(:results) { Gitlab::ProjectSearchResults.new(project, query, '') } + let(:results) { Gitlab::ProjectSearchResults.new(user, project, query, '') } it { expect(results.project).to eq(project) } it { expect(results.repository_ref).to be_nil } @@ -14,10 +15,74 @@ describe Gitlab::ProjectSearchResults, lib: true do describe 'initialize with ref' do let(:ref) { 'refs/heads/test' } - let(:results) { Gitlab::ProjectSearchResults.new(project, query, ref) } + let(:results) { Gitlab::ProjectSearchResults.new(user, project, query, ref) } it { expect(results.project).to eq(project) } it { expect(results.repository_ref).to eq(ref) } it { expect(results.query).to eq('hello world') } end + + describe 'confidential issues' do + let(:query) { 'issue' } + let(:author) { create(:user) } + let(:assignee) { create(:user) } + let(:non_member) { create(:user) } + let(:member) { create(:user) } + let(:admin) { create(:admin) } + let!(:issue) { create(:issue, project: project, title: 'Issue 1') } + let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) } + let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) } + + it 'should not list project confidential issues for non project members' do + results = described_class.new(non_member, project, query) + issues = results.objects('issues') + + expect(issues).to include issue + expect(issues).not_to include security_issue_1 + expect(issues).not_to include security_issue_2 + expect(results.issues_count).to eq 1 + end + + it 'should list project confidential issues for author' do + results = described_class.new(author, project, query) + issues = results.objects('issues') + + expect(issues).to include issue + expect(issues).to include security_issue_1 + expect(issues).not_to include security_issue_2 + expect(results.issues_count).to eq 2 + end + + it 'should list project confidential issues for assignee' do + results = described_class.new(assignee, project.id, query) + issues = results.objects('issues') + + expect(issues).to include issue + expect(issues).not_to include security_issue_1 + expect(issues).to include security_issue_2 + expect(results.issues_count).to eq 2 + end + + it 'should list project confidential issues for project members' do + project.team << [member, :developer] + + results = described_class.new(member, project, query) + issues = results.objects('issues') + + expect(issues).to include issue + expect(issues).to include security_issue_1 + expect(issues).to include security_issue_2 + expect(results.issues_count).to eq 3 + end + + it 'should list all project issues for admin' do + results = described_class.new(admin, project, query) + issues = results.objects('issues') + + expect(issues).to include issue + expect(issues).to include security_issue_1 + expect(issues).to include security_issue_2 + expect(results.issues_count).to eq 3 + end + end end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 7d963795e17..65af37e24f1 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe Gitlab::ReferenceExtractor, lib: true do let(:project) { create(:project) } + subject { Gitlab::ReferenceExtractor.new(project, project.creator) } it 'accesses valid user objects' do @@ -41,6 +42,7 @@ describe Gitlab::ReferenceExtractor, lib: true do end it 'accesses valid issue objects' do + project.team << [project.creator, :developer] @i0 = create(:issue, project: project) @i1 = create(:issue, project: project) diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index bb18f417858..f4afe597e8d 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe Gitlab::SearchResults do + let(:user) { create(:user) } let!(:project) { create(:project, name: 'foo') } let!(:issue) { create(:issue, project: project, title: 'foo') } @@ -9,7 +10,7 @@ describe Gitlab::SearchResults do end let!(:milestone) { create(:milestone, project: project, title: 'foo') } - let(:results) { described_class.new(Project.all, 'foo') } + let(:results) { described_class.new(user, Project.all, 'foo') } describe '#total_count' do it 'returns the total amount of search hits' do @@ -52,4 +53,92 @@ describe Gitlab::SearchResults do expect(results.empty?).to eq(false) end end + + describe 'confidential issues' do + let(:project_1) { create(:empty_project) } + let(:project_2) { create(:empty_project) } + let(:project_3) { create(:empty_project) } + let(:project_4) { create(:empty_project) } + let(:query) { 'issue' } + let(:limit_projects) { Project.where(id: [project_1.id, project_2.id, project_3.id]) } + let(:author) { create(:user) } + let(:assignee) { create(:user) } + let(:non_member) { create(:user) } + let(:member) { create(:user) } + let(:admin) { create(:admin) } + let!(:issue) { create(:issue, project: project_1, title: 'Issue 1') } + let!(:security_issue_1) { create(:issue, :confidential, project: project_1, title: 'Security issue 1', author: author) } + let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project_1, assignee: assignee) } + let!(:security_issue_3) { create(:issue, :confidential, project: project_2, title: 'Security issue 3', author: author) } + let!(:security_issue_4) { create(:issue, :confidential, project: project_3, title: 'Security issue 4', assignee: assignee) } + let!(:security_issue_5) { create(:issue, :confidential, project: project_4, title: 'Security issue 5') } + + it 'should not list confidential issues for non project members' do + results = described_class.new(non_member, limit_projects, query) + issues = results.objects('issues') + + expect(issues).to include issue + expect(issues).not_to include security_issue_1 + expect(issues).not_to include security_issue_2 + expect(issues).not_to include security_issue_3 + expect(issues).not_to include security_issue_4 + expect(issues).not_to include security_issue_5 + expect(results.issues_count).to eq 1 + end + + it 'should list confidential issues for author' do + results = described_class.new(author, limit_projects, query) + issues = results.objects('issues') + + expect(issues).to include issue + expect(issues).to include security_issue_1 + expect(issues).not_to include security_issue_2 + expect(issues).to include security_issue_3 + expect(issues).not_to include security_issue_4 + expect(issues).not_to include security_issue_5 + expect(results.issues_count).to eq 3 + end + + it 'should list confidential issues for assignee' do + results = described_class.new(assignee, limit_projects, query) + issues = results.objects('issues') + + expect(issues).to include issue + expect(issues).not_to include security_issue_1 + expect(issues).to include security_issue_2 + expect(issues).not_to include security_issue_3 + expect(issues).to include security_issue_4 + expect(issues).not_to include security_issue_5 + expect(results.issues_count).to eq 3 + end + + it 'should list confidential issues for project members' do + project_1.team << [member, :developer] + project_2.team << [member, :developer] + + results = described_class.new(member, limit_projects, query) + issues = results.objects('issues') + + expect(issues).to include issue + expect(issues).to include security_issue_1 + expect(issues).to include security_issue_2 + expect(issues).to include security_issue_3 + expect(issues).not_to include security_issue_4 + expect(issues).not_to include security_issue_5 + expect(results.issues_count).to eq 4 + end + + it 'should list all issues for admin' do + results = described_class.new(admin, limit_projects, query) + issues = results.objects('issues') + + expect(issues).to include issue + expect(issues).to include security_issue_1 + expect(issues).to include security_issue_2 + expect(issues).to include security_issue_3 + expect(issues).to include security_issue_4 + expect(issues).not_to include security_issue_5 + expect(results.issues_count).to eq 5 + end + end end diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb index 4799bbaa57c..ac12ab6c757 100644 --- a/spec/models/abuse_report_spec.rb +++ b/spec/models/abuse_report_spec.rb @@ -13,7 +13,8 @@ require 'rails_helper' RSpec.describe AbuseReport, type: :model do - subject { create(:abuse_report) } + subject { create(:abuse_report) } + let(:user) { create(:user) } it { expect(subject).to be_valid } @@ -31,17 +32,14 @@ RSpec.describe AbuseReport, type: :model do describe '#remove_user' do it 'blocks the user' do - report = build(:abuse_report) - - allow(report.user).to receive(:destroy) - - expect { report.remove_user }.to change { report.user.blocked? }.to(true) + expect { subject.remove_user(deleted_by: user) }.to change { subject.user.blocked? }.to(true) end - it 'removes the user' do - report = build(:abuse_report) + it 'lets a worker delete the user' do + expect(DeleteUserWorker).to receive(:perform_async).with(user.id, subject.user.id, + delete_solo_owned_groups: true) - expect { report.remove_user }.to change { User.count }.by(-1) + subject.remove_user(deleted_by: user) end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 253902512c3..0e9111c8029 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -86,10 +86,21 @@ eos let(:issue) { create :issue, project: project } let(:other_project) { create :project, :public } let(:other_issue) { create :issue, project: other_project } + let(:commiter) { create :user } + + before do + project.team << [commiter, :developer] + other_project.team << [commiter, :developer] + end it 'detects issues that this commit is marked as closing' do ext_ref = "#{other_project.path_with_namespace}##{other_issue.iid}" - allow(commit).to receive(:safe_message).and_return("Fixes ##{issue.iid} and #{ext_ref}") + + allow(commit).to receive_messages( + safe_message: "Fixes ##{issue.iid} and #{ext_ref}", + committer_email: commiter.email + ) + expect(commit.closes_issues).to include(issue) expect(commit.closes_issues).to include(other_issue) end diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb index 20f0c561e44..cb33edde820 100644 --- a/spec/models/concerns/mentionable_spec.rb +++ b/spec/models/concerns/mentionable_spec.rb @@ -48,7 +48,8 @@ describe Issue, "Mentionable" do describe '#create_new_cross_references!' do let(:project) { create(:project) } - let(:issues) { create_list(:issue, 2, project: project) } + let(:author) { create(:author) } + let(:issues) { create_list(:issue, 2, project: project, author: author) } context 'before changes are persisted' do it 'ignores pre-existing references' do @@ -91,7 +92,7 @@ describe Issue, "Mentionable" do end def create_issue(description:) - create(:issue, project: project, description: description) + create(:issue, project: project, description: description, author: author) end end end diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb new file mode 100644 index 00000000000..47c3be673c5 --- /dev/null +++ b/spec/models/concerns/milestoneish_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +describe Milestone, 'Milestoneish' do + let(:author) { create(:user) } + let(:assignee) { create(:user) } + let(:non_member) { create(:user) } + let(:member) { create(:user) } + let(:admin) { create(:admin) } + let(:project) { create(:project, :public) } + let(:milestone) { create(:milestone, project: project) } + let!(:issue) { create(:issue, project: project, milestone: milestone) } + let!(:security_issue_1) { create(:issue, :confidential, project: project, author: author, milestone: milestone) } + let!(:security_issue_2) { create(:issue, :confidential, project: project, assignee: assignee, milestone: milestone) } + let!(:closed_issue_1) { create(:issue, :closed, project: project, milestone: milestone) } + let!(:closed_issue_2) { create(:issue, :closed, project: project, milestone: milestone) } + let!(:closed_security_issue_1) { create(:issue, :confidential, :closed, project: project, author: author, milestone: milestone) } + let!(:closed_security_issue_2) { create(:issue, :confidential, :closed, project: project, assignee: assignee, milestone: milestone) } + let!(:closed_security_issue_3) { create(:issue, :confidential, :closed, project: project, author: author, milestone: milestone) } + let!(:closed_security_issue_4) { create(:issue, :confidential, :closed, project: project, assignee: assignee, milestone: milestone) } + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) } + + before do + project.team << [member, :developer] + end + + describe '#closed_items_count' do + it 'should not count confidential issues for non project members' do + expect(milestone.closed_items_count(non_member)).to eq 2 + end + + it 'should count confidential issues for author' do + expect(milestone.closed_items_count(author)).to eq 4 + end + + it 'should count confidential issues for assignee' do + expect(milestone.closed_items_count(assignee)).to eq 4 + end + + it 'should count confidential issues for project members' do + expect(milestone.closed_items_count(member)).to eq 6 + end + + it 'should count all issues for admin' do + expect(milestone.closed_items_count(admin)).to eq 6 + end + end + + describe '#total_items_count' do + it 'should not count confidential issues for non project members' do + expect(milestone.total_items_count(non_member)).to eq 4 + end + + it 'should count confidential issues for author' do + expect(milestone.total_items_count(author)).to eq 7 + end + + it 'should count confidential issues for assignee' do + expect(milestone.total_items_count(assignee)).to eq 7 + end + + it 'should count confidential issues for project members' do + expect(milestone.total_items_count(member)).to eq 10 + end + + it 'should count all issues for admin' do + expect(milestone.total_items_count(admin)).to eq 10 + end + end + + describe '#complete?' do + it 'returns false when has items opened' do + expect(milestone.complete?(non_member)).to eq false + end + + it 'returns true when all items are closed' do + issue.close + merge_request.close + + expect(milestone.complete?(non_member)).to eq true + end + end + + describe '#percent_complete' do + it 'should not count confidential issues for non project members' do + expect(milestone.percent_complete(non_member)).to eq 50 + end + + it 'should count confidential issues for author' do + expect(milestone.percent_complete(author)).to eq 57 + end + + it 'should count confidential issues for assignee' do + expect(milestone.percent_complete(assignee)).to eq 57 + end + + it 'should count confidential issues for project members' do + expect(milestone.percent_complete(member)).to eq 60 + end + + it 'should count confidential issues for admin' do + expect(milestone.percent_complete(admin)).to eq 60 + end + end +end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index ec2a923f91b..5fe44246738 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -65,6 +65,42 @@ describe Event, models: true do it { expect(@event.author).to eq(@user) } end + describe '#proper?' do + context 'issue event' do + let(:project) { create(:empty_project, :public) } + let(:non_member) { create(:user) } + let(:member) { create(:user) } + let(:author) { create(:author) } + let(:assignee) { create(:user) } + let(:admin) { create(:admin) } + let(:event) { Event.new(project: project, action: Event::CREATED, target: issue, author_id: author.id) } + + before do + project.team << [member, :developer] + end + + context 'for non confidential issues' do + let(:issue) { create(:issue, project: project, author: author, assignee: assignee) } + + it { expect(event.proper?(non_member)).to eq true } + it { expect(event.proper?(author)).to eq true } + it { expect(event.proper?(assignee)).to eq true } + it { expect(event.proper?(member)).to eq true } + it { expect(event.proper?(admin)).to eq true } + end + + context 'for confidential issues' do + let(:issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) } + + it { expect(event.proper?(non_member)).to eq false } + it { expect(event.proper?(author)).to eq true } + it { expect(event.proper?(assignee)).to eq true } + it { expect(event.proper?(member)).to eq true } + it { expect(event.proper?(admin)).to eq true } + end + end + end + describe '.limit_recent' do let!(:event1) { create(:closed_issue_event) } let!(:event2) { create(:closed_issue_event) } diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 7f44ca2f7db..540a62eb1f8 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -130,6 +130,15 @@ describe Issue, models: true do end end + describe '#related_branches' do + it "selects the right branches" do + allow(subject.project.repository).to receive(:branch_names). + and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name]) + + expect(subject.related_branches).to eq [subject.to_branch_name] + end + end + it_behaves_like 'an editable mentionable' do subject { create(:issue) } @@ -140,4 +149,12 @@ describe Issue, models: true do it_behaves_like 'a Taskable' do let(:subject) { create :issue } end + + describe "#to_branch_name" do + let(:issue) { create(:issue, title: 'a' * 30) } + + it "starts with the issue iid" do + expect(issue.to_branch_name).to match /-#{issue.iid}\z/ + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 8bf68013fd2..f2f07e4ee17 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -86,6 +86,41 @@ describe MergeRequest, models: true do end end + describe '#target_sha' do + context 'when the target branch does not exist anymore' do + subject { create(:merge_request).tap { |mr| mr.update_attribute(:target_branch, 'deleted') } } + + it 'returns nil' do + expect(subject.target_sha).to be_nil + end + end + end + + describe '#source_sha' do + let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) } + + context 'with diffs' do + subject { create(:merge_request, :with_diffs) } + it 'returns the sha of the source branch last commit' do + expect(subject.source_sha).to eq(last_branch_commit.sha) + end + end + + context 'without diffs' do + subject { create(:merge_request, :without_diffs) } + it 'returns the sha of the source branch last commit' do + expect(subject.source_sha).to eq(last_branch_commit.sha) + end + end + + context 'when the merge request is being created' do + subject { build(:merge_request, source_branch: nil, compare_commits: []) } + it 'returns nil' do + expect(subject.source_sha).to be_nil + end + end + end + describe '#to_reference' do it 'returns a String reference to the object' do expect(subject.to_reference).to eq "!#{subject.iid}" @@ -150,6 +185,7 @@ describe MergeRequest, models: true do let(:commit2) { double('commit2', safe_message: "Fixes #{issue1.to_reference}") } before do + subject.project.team << [subject.author, :developer] allow(subject).to receive(:commits).and_return([commit0, commit1, commit2]) end @@ -180,24 +216,11 @@ describe MergeRequest, models: true do end describe "#work_in_progress?" do - it "detects the 'WIP ' prefix" do - subject.title = "WIP #{subject.title}" - expect(subject).to be_work_in_progress - end - - it "detects the 'WIP: ' prefix" do - subject.title = "WIP: #{subject.title}" - expect(subject).to be_work_in_progress - end - - it "detects the '[WIP] ' prefix" do - subject.title = "[WIP] #{subject.title}" - expect(subject).to be_work_in_progress - end - - it "detects the '[WIP]' prefix" do - subject.title = "[WIP]#{subject.title}" - expect(subject).to be_work_in_progress + ['WIP ', 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', ' [WIP] WIP [WIP] WIP: WIP '].each do |wip_prefix| + it "detects the '#{wip_prefix}' prefix" do + subject.title = "#{wip_prefix}#{subject.title}" + expect(subject).to be_work_in_progress + end end it "doesn't detect WIP for words starting with WIP" do @@ -205,6 +228,11 @@ describe MergeRequest, models: true do expect(subject).not_to be_work_in_progress end + it "doesn't detect WIP for words containing with WIP" do + subject.title = "WupWipwap #{subject.title}" + expect(subject).not_to be_work_in_progress + end + it "doesn't detect WIP by default" do expect(subject).not_to be_work_in_progress end @@ -284,6 +312,18 @@ describe MergeRequest, models: true do let(:project) { create(:project) } let(:fork_project) { create(:project, forked_from_project: project) } + context 'when the target branch does not exist anymore' do + subject { create(:merge_request).tap { |mr| mr.update_attribute(:target_branch, 'deleted') } } + + it 'does not crash' do + expect{ subject.diverged_commits_count }.not_to raise_error + end + + it 'returns 0' do + expect(subject.diverged_commits_count).to eq(0) + end + end + context 'diverged on same repository' do subject(:merge_request_with_divergence) { create(:merge_request, :diverged, source_project: project, target_project: project) } diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index de1757bf67a..72a4ea70228 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -32,6 +32,7 @@ describe Milestone, models: true do let(:milestone) { create(:milestone) } let(:issue) { create(:issue) } + let(:user) { create(:user) } describe "unique milestone title per project" do it "shouldn't accept the same title in a project twice" do @@ -50,18 +51,17 @@ describe Milestone, models: true do describe "#percent_complete" do it "should not count open issues" do milestone.issues << issue - expect(milestone.percent_complete).to eq(0) + expect(milestone.percent_complete(user)).to eq(0) end it "should count closed issues" do issue.close milestone.issues << issue - expect(milestone.percent_complete).to eq(100) + expect(milestone.percent_complete(user)).to eq(100) end it "should recover from dividing by zero" do - expect(milestone.issues).to receive(:size).and_return(0) - expect(milestone.percent_complete).to eq(0) + expect(milestone.percent_complete(user)).to eq(0) end end @@ -103,7 +103,7 @@ describe Milestone, models: true do ) end - it { expect(milestone.percent_complete).to eq(75) } + it { expect(milestone.percent_complete(user)).to eq(75) } end describe :items_count do @@ -113,23 +113,23 @@ describe Milestone, models: true do milestone.merge_requests << create(:merge_request) end - it { expect(milestone.closed_items_count).to eq(1) } - it { expect(milestone.total_items_count).to eq(3) } - it { expect(milestone.is_empty?).to be_falsey } + it { expect(milestone.closed_items_count(user)).to eq(1) } + it { expect(milestone.total_items_count(user)).to eq(3) } + it { expect(milestone.is_empty?(user)).to be_falsey } end describe :can_be_closed? do it { expect(milestone.can_be_closed?).to be_truthy } end - describe :is_empty? do + describe :total_items_count do before do create :closed_issue, milestone: milestone create :merge_request, milestone: milestone end it 'Should return total count of issues and merge requests assigned to milestone' do - expect(milestone.total_items_count).to eq 2 + expect(milestone.total_items_count(user)).to eq 2 end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 34866be3395..a57229a4fdf 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -101,13 +101,29 @@ describe Repository, models: true do end describe 'parsing result' do - subject { repository.parse_search_result(results.first) } + subject { repository.parse_search_result(search_result) } + let(:search_result) { results.first } it { is_expected.to be_an OpenStruct } it { expect(subject.filename).to eq('CHANGELOG') } + it { expect(subject.basename).to eq('CHANGELOG') } it { expect(subject.ref).to eq('master') } it { expect(subject.startline).to eq(186) } it { expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") } + + context "when filename has extension" do + let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" } + + it { expect(subject.filename).to eq('CONTRIBUTE.md') } + it { expect(subject.basename).to eq('CONTRIBUTE') } + end + + context "when file under directory" do + let(:search_result) { "master:a/b/c.md:5:a b c\n" } + + it { expect(subject.filename).to eq('a/b/c.md') } + it { expect(subject.basename).to eq('a/b/c') } + end end end @@ -581,9 +597,9 @@ describe Repository, models: true do describe '#after_push_commit' do it 'flushes the cache' do - expect(repository).to receive(:expire_cache).with('master') + expect(repository).to receive(:expire_cache).with('master', '123') - repository.after_push_commit('master') + repository.after_push_commit('master', '123') end end @@ -687,4 +703,111 @@ describe Repository, models: true do repository.rm_tag('8.5') end end + + describe '#avatar' do + it 'returns the first avatar file found in the repository' do + expect(repository).to receive(:blob_at_branch). + with('master', 'logo.png'). + and_return(true) + + expect(repository.avatar).to eq('logo.png') + end + + it 'caches the output' do + allow(repository).to receive(:blob_at_branch). + with('master', 'logo.png'). + and_return(true) + + expect(repository.avatar).to eq('logo.png') + + expect(repository).to_not receive(:blob_at_branch) + expect(repository.avatar).to eq('logo.png') + end + end + + describe '#expire_avatar_cache' do + let(:cache) { repository.send(:cache) } + + before do + allow(repository).to receive(:cache).and_return(cache) + end + + context 'without a branch or revision' do + it 'flushes the cache' do + expect(cache).to receive(:expire).with(:avatar) + + repository.expire_avatar_cache + end + end + + context 'with a branch' do + it 'does not flush the cache if the branch is not the default branch' do + expect(cache).not_to receive(:expire) + + repository.expire_avatar_cache('cats') + end + + it 'flushes the cache if the branch equals the default branch' do + expect(cache).to receive(:expire).with(:avatar) + + repository.expire_avatar_cache(repository.root_ref) + end + end + + context 'with a branch and revision' do + let(:commit) { double(:commit) } + + before do + allow(repository).to receive(:commit).and_return(commit) + end + + it 'does not flush the cache if the commit does not change any logos' do + diff = double(:diff, new_path: 'test.txt') + + expect(commit).to receive(:diffs).and_return([diff]) + expect(cache).not_to receive(:expire) + + repository.expire_avatar_cache(repository.root_ref, '123') + end + + it 'flushes the cache if the commit changes any of the logos' do + diff = double(:diff, new_path: Repository::AVATAR_FILES[0]) + + expect(commit).to receive(:diffs).and_return([diff]) + expect(cache).to receive(:expire).with(:avatar) + + repository.expire_avatar_cache(repository.root_ref, '123') + end + end + end + + describe '#build_cache' do + let(:cache) { repository.send(:cache) } + + it 'builds the caches if they do not already exist' do + expect(cache).to receive(:exist?). + exactly(repository.cache_keys.length). + times. + and_return(false) + + repository.cache_keys.each do |key| + expect(repository).to receive(key) + end + + repository.build_cache + end + + it 'does not build any caches that already exist' do + expect(cache).to receive(:exist?). + exactly(repository.cache_keys.length). + times. + and_return(true) + + repository.cache_keys.each do |key| + expect(repository).to_not receive(key) + end + + repository.build_cache + end + end end diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb index fe9ea7e7d1e..d9b86b9368f 100644 --- a/spec/models/todo_spec.rb +++ b/spec/models/todo_spec.rb @@ -5,19 +5,24 @@ # id :integer not null, primary key # user_id :integer not null # project_id :integer not null -# target_id :integer not null +# target_id :integer # target_type :string not null # author_id :integer -# note_id :integer # action :integer not null # state :string not null # created_at :datetime # updated_at :datetime +# note_id :integer +# commit_id :string # require 'spec_helper' describe Todo, models: true do + let(:project) { create(:project) } + let(:commit) { project.commit } + let(:issue) { create(:issue) } + describe 'relationships' do it { is_expected.to belong_to(:author).class_name("User") } it { is_expected.to belong_to(:note) } @@ -33,8 +38,22 @@ describe Todo, models: true do describe 'validations' do it { is_expected.to validate_presence_of(:action) } - it { is_expected.to validate_presence_of(:target) } + it { is_expected.to validate_presence_of(:target_type) } it { is_expected.to validate_presence_of(:user) } + + context 'for commits' do + subject { described_class.new(target_type: 'Commit') } + + it { is_expected.to validate_presence_of(:commit_id) } + it { is_expected.not_to validate_presence_of(:target_id) } + end + + context 'for issuables' do + subject { described_class.new(target: issue) } + + it { is_expected.to validate_presence_of(:target_id) } + it { is_expected.not_to validate_presence_of(:commit_id) } + end end describe '#body' do @@ -55,15 +74,69 @@ describe Todo, models: true do end end - describe '#done!' do + describe '#done' do it 'changes state to done' do todo = create(:todo, state: :pending) - expect { todo.done! }.to change(todo, :state).from('pending').to('done') + expect { todo.done }.to change(todo, :state).from('pending').to('done') end it 'does not raise error when is already done' do todo = create(:todo, state: :done) - expect { todo.done! }.not_to raise_error + expect { todo.done }.not_to raise_error + end + end + + describe '#for_commit?' do + it 'returns true when target is a commit' do + subject.target_type = 'Commit' + expect(subject.for_commit?).to eq true + end + + it 'returns false when target is an issuable' do + subject.target_type = 'Issue' + expect(subject.for_commit?).to eq false + end + end + + describe '#target' do + context 'for commits' do + it 'returns an instance of Commit when exists' do + subject.project = project + subject.target_type = 'Commit' + subject.commit_id = commit.id + + expect(subject.target).to be_a(Commit) + expect(subject.target).to eq commit + end + + it 'returns nil when does not exists' do + subject.project = project + subject.target_type = 'Commit' + subject.commit_id = 'xxxx' + + expect(subject.target).to be_nil + end + end + + it 'returns the issuable for issuables' do + subject.target_id = issue.id + subject.target_type = issue.class.name + expect(subject.target).to eq issue + end + end + + describe '#target_reference' do + it 'returns the short commit id for commits' do + subject.project = project + subject.target_type = 'Commit' + subject.commit_id = commit.id + + expect(subject.target_reference).to eq commit.short_id + end + + it 'returns reference for issuables' do + subject.target = issue + expect(subject.target_reference).to eq issue.to_reference end end end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 571ea2dae4c..bb2ab058003 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -3,7 +3,11 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } - let!(:project) { create(:project, namespace: user.namespace ) } + let(:non_member) { create(:user) } + let(:author) { create(:author) } + let(:assignee) { create(:assignee) } + let(:admin) { create(:admin) } + let!(:project) { create(:project, :public, namespace: user.namespace ) } let!(:closed_issue) do create :closed_issue, author: user, @@ -12,6 +16,13 @@ describe API::API, api: true do state: :closed, milestone: milestone end + let!(:confidential_issue) do + create :issue, + :confidential, + project: project, + author: author, + assignee: assignee + end let!(:issue) do create :issue, author: user, @@ -123,10 +134,43 @@ describe API::API, api: true do let(:base_url) { "/projects/#{project.id}" } let(:title) { milestone.title } - it "should return project issues" do + it 'should return project issues without confidential issues for non project members' do + get api("#{base_url}/issues", non_member) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['title']).to eq(issue.title) + end + + it 'should return project confidential issues for author' do + get api("#{base_url}/issues", author) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.first['title']).to eq(issue.title) + end + + it 'should return project confidential issues for assignee' do + get api("#{base_url}/issues", assignee) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.first['title']).to eq(issue.title) + end + + it 'should return project issues with confidential issues for project members' do get api("#{base_url}/issues", user) expect(response.status).to eq(200) expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.first['title']).to eq(issue.title) + end + + it 'should return project confidential issues for admin' do + get api("#{base_url}/issues", admin) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) expect(json_response.first['title']).to eq(issue.title) end @@ -206,6 +250,41 @@ describe API::API, api: true do get api("/projects/#{project.id}/issues/54321", user) expect(response.status).to eq(404) end + + context 'confidential issues' do + it "should return 404 for non project members" do + get api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member) + expect(response.status).to eq(404) + end + + it "should return confidential issue for project members" do + get api("/projects/#{project.id}/issues/#{confidential_issue.id}", user) + expect(response.status).to eq(200) + expect(json_response['title']).to eq(confidential_issue.title) + expect(json_response['iid']).to eq(confidential_issue.iid) + end + + it "should return confidential issue for author" do + get api("/projects/#{project.id}/issues/#{confidential_issue.id}", author) + expect(response.status).to eq(200) + expect(json_response['title']).to eq(confidential_issue.title) + expect(json_response['iid']).to eq(confidential_issue.iid) + end + + it "should return confidential issue for assignee" do + get api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee) + expect(response.status).to eq(200) + expect(json_response['title']).to eq(confidential_issue.title) + expect(json_response['iid']).to eq(confidential_issue.iid) + end + + it "should return confidential issue for admin" do + get api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin) + expect(response.status).to eq(200) + expect(json_response['title']).to eq(confidential_issue.title) + expect(json_response['iid']).to eq(confidential_issue.iid) + end + end end describe "POST /projects/:id/issues" do @@ -294,6 +373,35 @@ describe API::API, api: true do expect(response.status).to eq(400) expect(json_response['message']['labels']['?']['title']).to eq(['is invalid']) end + + context 'confidential issues' do + it "should return 403 for non project members" do + put api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member), + title: 'updated title' + expect(response.status).to eq(403) + end + + it "should update a confidential issue for project members" do + put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), + title: 'updated title' + expect(response.status).to eq(200) + expect(json_response['title']).to eq('updated title') + end + + it "should update a confidential issue for author" do + put api("/projects/#{project.id}/issues/#{confidential_issue.id}", author), + title: 'updated title' + expect(response.status).to eq(200) + expect(json_response['title']).to eq('updated title') + end + + it "should update a confidential issue for admin" do + put api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin), + title: 'updated title' + expect(response.status).to eq(200) + expect(json_response['title']).to eq('updated title') + end + end end describe 'PUT /projects/:id/issues/:issue_id to update labels' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 5366a7bd06b..679227bf881 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -282,6 +282,13 @@ describe API::API, api: true do expect(user.reload.admin).to eq(true) end + it "should update external status" do + put api("/users/#{user.id}", admin), { external: true } + expect(response.status).to eq 200 + expect(json_response['external']).to eq(true) + expect(user.reload.external?).to be_truthy + end + it "should not update admin status" do put api("/users/#{admin_user.id}", admin), { can_create_group: false } expect(response.status).to eq(200) diff --git a/spec/services/delete_user_service_spec.rb b/spec/services/delete_user_service_spec.rb new file mode 100644 index 00000000000..a65938fa03b --- /dev/null +++ b/spec/services/delete_user_service_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe DeleteUserService, services: true do + describe "Deletes a user and all their personal projects" do + let!(:user) { create(:user) } + let!(:current_user) { create(:user) } + let!(:namespace) { create(:namespace, owner: user) } + let!(:project) { create(:project, namespace: namespace) } + + context 'no options are given' do + it 'deletes the user' do + DeleteUserService.new(current_user).execute(user) + + expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'will delete the project in the near future' do + expect_any_instance_of(Projects::DestroyService).to receive(:pending_delete!).once + + DeleteUserService.new(current_user).execute(user) + end + end + + context "solo owned groups present" do + let(:solo_owned) { create(:group) } + let(:member) { create(:group_member) } + let(:user) { member.user } + + before do + solo_owned.group_members = [member] + DeleteUserService.new(current_user).execute(user) + end + + it 'does not delete the user' do + expect(User.find(user.id)).to eq user + end + end + + context "deletions with solo owned groups" do + let(:solo_owned) { create(:group) } + let(:member) { create(:group_member) } + let(:user) { member.user } + + before do + solo_owned.group_members = [member] + DeleteUserService.new(current_user).execute(user, delete_solo_owned_groups: true) + end + + it 'deletes solo owned groups' do + expect { Project.find(solo_owned.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'deletes the user' do + expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end +end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 145bc937560..8490a729e51 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -29,7 +29,8 @@ describe GitPushService, services: true do it { is_expected.to be_truthy } it 'flushes general cached data' do - expect(project.repository).to receive(:expire_cache).with('master') + expect(project.repository).to receive(:expire_cache). + with('master', newrev) subject end @@ -46,7 +47,8 @@ describe GitPushService, services: true do it { is_expected.to be_truthy } it 'flushes general cached data' do - expect(project.repository).to receive(:expire_cache).with('master') + expect(project.repository).to receive(:expire_cache). + with('master', newrev) subject end @@ -65,7 +67,8 @@ describe GitPushService, services: true do end it 'flushes general cached data' do - expect(project.repository).to receive(:expire_cache).with('master') + expect(project.repository).to receive(:expire_cache). + with('master', newrev) subject end @@ -212,12 +215,16 @@ describe GitPushService, services: true do let(:commit) { project.commit } before do + project.team << [commit_author, :developer] + project.team << [user, :developer] + allow(commit).to receive_messages( safe_message: "this commit \n mentions #{issue.to_reference}", references: [issue], author_name: commit_author.name, author_email: commit_author.email ) + allow(project.repository).to receive(:commits_between).and_return([commit]) end diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb new file mode 100644 index 00000000000..6108c26a78b --- /dev/null +++ b/spec/services/projects/autocomplete_service_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe Projects::AutocompleteService, services: true do + describe '#issues' do + describe 'confidential issues' do + let(:author) { create(:user) } + let(:assignee) { create(:user) } + let(:non_member) { create(:user) } + let(:member) { create(:user) } + let(:admin) { create(:admin) } + let(:project) { create(:empty_project, :public) } + let!(:issue) { create(:issue, project: project, title: 'Issue 1') } + let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) } + let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) } + + it 'should not list project confidential issues for guests' do + autocomplete = described_class.new(project, nil) + issues = autocomplete.issues.map(&:iid) + + expect(issues).to include issue.iid + expect(issues).not_to include security_issue_1.iid + expect(issues).not_to include security_issue_2.iid + expect(issues.count).to eq 1 + end + + it 'should not list project confidential issues for non project members' do + autocomplete = described_class.new(project, non_member) + issues = autocomplete.issues.map(&:iid) + + expect(issues).to include issue.iid + expect(issues).not_to include security_issue_1.iid + expect(issues).not_to include security_issue_2.iid + expect(issues.count).to eq 1 + end + + it 'should list project confidential issues for author' do + autocomplete = described_class.new(project, author) + issues = autocomplete.issues.map(&:iid) + + expect(issues).to include issue.iid + expect(issues).to include security_issue_1.iid + expect(issues).not_to include security_issue_2.iid + expect(issues.count).to eq 2 + end + + it 'should list project confidential issues for assignee' do + autocomplete = described_class.new(project, assignee) + issues = autocomplete.issues.map(&:iid) + + expect(issues).to include issue.iid + expect(issues).not_to include security_issue_1.iid + expect(issues).to include security_issue_2.iid + expect(issues.count).to eq 2 + end + + it 'should list project confidential issues for project members' do + project.team << [member, :developer] + + autocomplete = described_class.new(project, member) + issues = autocomplete.issues.map(&:iid) + + expect(issues).to include issue.iid + expect(issues).to include security_issue_1.iid + expect(issues).to include security_issue_2.iid + expect(issues.count).to eq 3 + end + + it 'should list all project issues for admin' do + autocomplete = described_class.new(project, admin) + issues = autocomplete.issues.map(&:iid) + + expect(issues).to include issue.iid + expect(issues).to include security_issue_1.iid + expect(issues).to include security_issue_2.iid + expect(issues.count).to eq 3 + end + end + end +end diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb index 93bf1b81fbe..4c5ced7e746 100644 --- a/spec/services/projects/housekeeping_service_spec.rb +++ b/spec/services/projects/housekeeping_service_spec.rb @@ -12,7 +12,7 @@ describe Projects::HousekeepingService do it 'enqueues a sidekiq job' do expect(subject).to receive(:try_obtain_lease).and_return(true) - expect(GitlabShellWorker).to receive(:perform_async).with(:gc, project.path_with_namespace) + expect(GitlabShellOneShotWorker).to receive(:perform_async).with(:gc, project.path_with_namespace) subject.execute expect(project.pushes_since_gc).to eq(0) @@ -20,7 +20,7 @@ describe Projects::HousekeepingService do it 'does not enqueue a job when no lease can be obtained' do expect(subject).to receive(:try_obtain_lease).and_return(false) - expect(GitlabShellWorker).not_to receive(:perform_async) + expect(GitlabShellOneShotWorker).not_to receive(:perform_async) expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken) expect(project.pushes_since_gc).to eq(0) diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 5dcc39f5fdc..8e6292014d4 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -280,6 +280,18 @@ describe SystemNoteService, services: true do end end + describe '.new_issue_branch' do + subject { described_class.new_issue_branch(noteable, project, author, "1-mepmep") } + + it_behaves_like 'a system note' + + context 'when a branch is created from the new branch button' do + it 'sets the note text' do + expect(subject.note).to match /\AStarted branch [`1-mepmep`]/ + end + end + end + describe '.cross_reference' do subject { described_class.cross_reference(noteable, mentioner, author) } diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 96420acb31d..b4728807b8b 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -148,8 +148,13 @@ describe TodoService, services: true do should_not_create_todo(user: stranger, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) end - it 'does not create todo when leaving a note on commit' do - should_not_create_any_todo { service.new_note(note_on_commit, john_doe) } + it 'creates a todo for each valid mentioned user when leaving a note on commit' do + service.new_note(note_on_commit, john_doe) + + should_create_todo(user: michael, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) + should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) + should_not_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) + should_not_create_todo(user: stranger, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) end it 'does not create todo when leaving a note on snippet' do diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 65d59e6813c..e1f90e17cce 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -10,7 +10,7 @@ Capybara.register_driver :poltergeist do |app| Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout, window_size: [1366, 768]) end -Capybara.default_wait_time = timeout +Capybara.default_max_wait_time = timeout Capybara.ignore_hidden_elements = true unless ENV['CI'] || ENV['CI_SERVER'] diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index fce91015fd4..e876d44c166 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/mentionable_shared_examples.rb @@ -52,6 +52,8 @@ shared_context 'mentionable context' do end set_mentionable_text.call(ref_string) + + project.team << [author, :developer] end end diff --git a/spec/support/wait_for_ajax.rb b/spec/support/wait_for_ajax.rb index 692d219e9f1..b90fc112671 100644 --- a/spec/support/wait_for_ajax.rb +++ b/spec/support/wait_for_ajax.rb @@ -1,6 +1,6 @@ module WaitForAjax def wait_for_ajax - Timeout.timeout(Capybara.default_wait_time) do + Timeout.timeout(Capybara.default_max_wait_time) do loop until finished_all_ajax_requests? end end diff --git a/spec/workers/delete_user_worker_spec.rb b/spec/workers/delete_user_worker_spec.rb new file mode 100644 index 00000000000..14c56521280 --- /dev/null +++ b/spec/workers/delete_user_worker_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe DeleteUserWorker do + let!(:user) { create(:user) } + let!(:current_user) { create(:user) } + + it "calls the DeleteUserWorker with the params it was given" do + expect_any_instance_of(DeleteUserService).to receive(:execute). + with(user, {}) + + DeleteUserWorker.new.perform(current_user.id, user.id) + end + + it "uses symbolized keys" do + expect_any_instance_of(DeleteUserService).to receive(:execute). + with(user, test: "test") + + DeleteUserWorker.new.perform(current_user.id, user.id, "test" => "test") + end +end |