diff options
author | James Lopez <james@jameslopez.es> | 2016-05-03 13:14:31 +0200 |
---|---|---|
committer | James Lopez <james@jameslopez.es> | 2016-05-03 13:14:31 +0200 |
commit | 3929972d900ee2b0eca806c36074c93ac83af2fd (patch) | |
tree | 0a6d347b5c32facc0a5d38645e24918b2cff7c98 | |
parent | 5a4f576359a71a57c70544568d9291cba53a2d39 (diff) | |
parent | e172028a3615d1a7f1520ce65cddcdf707a16e5f (diff) | |
download | gitlab-ce-3929972d900ee2b0eca806c36074c93ac83af2fd.tar.gz |
Merge branch 'feature/project-import' of gitlab.com:gitlab-org/gitlab-ce into feature/project-export-ui-experimental
# Conflicts:
# app/controllers/import/gitlab_project_controller.rb
228 files changed, 3411 insertions, 1396 deletions
diff --git a/.scss-lint.yml b/.scss-lint.yml index 9bfc18b9698..66f9975d4ce 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -244,11 +244,11 @@ linters: # URLs should be valid and not contain protocols or domain names. UrlFormat: - enabled: false + enabled: true # URLs should always be enclosed within quotes. UrlQuotes: - enabled: false + enabled: true # Properties, like color and font, are easier to read and maintain # when defined using variables rather than literals. diff --git a/CHANGELOG b/CHANGELOG index fbc3ee194cb..b6527780bbf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,14 +1,36 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.8.0 (unreleased) + - Project#open_branches has been cleaned up and no longer loads entire records into memory. + - Make build status canceled if any of the jobs was canceled and none failed - Remove future dates from contribution calendar graph. - -v 8.7.1 (unreleased) + - Support e-mail notifications for comments on project snippets + - Use ActionDispatch Remote IP for Akismet checking + - Fix error when visiting commit builds page before build was updated + - Add 'l' shortcut to open Label dropdown on issuables and 'i' to create new issue on a project + - Updated search UI + - Display informative message when new milestone is created + - Replace Devise Async with Devise ActiveJob integration. !3902 (Connor Shea) + - Allow "NEWS" and "CHANGES" as alternative names for CHANGELOG. !3768 (Connor Shea) + - Added button to toggle whitespaces changes on diff view + - Backport GitLab Enterprise support from EE + - Files over 5MB can only be viewed in their raw form, files over 1MB without highlighting !3718 + - Add support for supressing text diffs using .gitattributes on the default branch (Matt Oakes) + - Added multiple colors for labels in dropdowns when dups happen. + +v 8.7.2 (unreleased) + - The "New Branch" button is now loaded asynchronously + - Fix error 500 when trying to create a wiki page + +v 8.7.1 + - Throttle the update of `project.last_activity_at` to 1 minute. !3848 - Fix .gitlab-ci.yml parsing issue when hidde job is a template without script definition. !3849 - Fix license detection to detect all license files, not only known licenses. !3878 - Use the `can?` helper instead of `current_user.can?`. !3882 - Prevent users from deleting Webhooks via API they do not own - Fix Error 500 due to stale cache when projects are renamed or transferred + - Update width of search box to fix Safari bug. !3900 (Jedidiah) + - Use the `can?` helper instead of `current_user.can?` v 8.7.0 - Gitlab::GitAccess and Gitlab::GitAccessWiki are now instrumented @@ -120,13 +142,25 @@ v 8.7.0 - Import GitHub labels - Add option to filter by "Owned projects" on dashboard page - Import GitHub milestones - - Fix emoji catgories in the emoji picker - Execute system web hooks on push to the project - Allow enable/disable push events for system hooks - Fix GitHub project's link in the import page when provider has a custom URL - Add RAW build trace output and button on build page - Add incremental build trace update into CI API +v 8.6.8 + - Prevent privilege escalation via "impersonate" feature + - Prevent privilege escalation via notes API + - Prevent privilege escalation via project webhook API + - Prevent XSS via Git branch and tag names + - Prevent XSS via custom issue tracker URL + - Prevent XSS via `window.opener` + - Prevent XSS via label drop-down + - Prevent information disclosure via milestone API + - Prevent information disclosure via snippet API + - Prevent information disclosure via project labels + - Prevent information disclosure via new merge request page + v 8.6.7 - Fix persistent XSS vulnerability in `commit_person_link` helper - Fix persistent XSS vulnerability in Label and Milestone dropdowns @@ -268,6 +302,17 @@ v 8.6.0 - Trigger a todo for mentions on commits page - Let project owners and admins soft delete issues and merge requests +v 8.5.12 + - Prevent privilege escalation via "impersonate" feature + - Prevent privilege escalation via notes API + - Prevent privilege escalation via project webhook API + - Prevent XSS via Git branch and tag names + - Prevent XSS via custom issue tracker URL + - Prevent XSS via `window.opener` + - Prevent information disclosure via snippet API + - Prevent information disclosure via project labels + - Prevent information disclosure via new merge request page + v 8.5.11 - Fix persistent XSS vulnerability in `commit_person_link` helper @@ -418,6 +463,17 @@ v 8.5.0 - Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul) - Add Todos +v 8.4.10 + - Prevent privilege escalation via "impersonate" feature + - Prevent privilege escalation via notes API + - Prevent privilege escalation via project webhook API + - Prevent XSS via Git branch and tag names + - Prevent XSS via custom issue tracker URL + - Prevent XSS via `window.opener` + - Prevent information disclosure via snippet API + - Prevent information disclosure via project labels + - Prevent information disclosure via new merge request page + v 8.4.9 - Fix persistent XSS vulnerability in `commit_person_link` helper @@ -543,6 +599,15 @@ v 8.4.0 - Add IP check against DNSBLs at account sign-up - Added cache:key to .gitlab-ci.yml allowing to fine tune the caching +v 8.3.9 + - Prevent privilege escalation via "impersonate" feature + - Prevent privilege escalation via notes API + - Prevent privilege escalation via project webhook API + - Prevent XSS via custom issue tracker URL + - Prevent XSS via `window.opener` + - Prevent information disclosure via project labels + - Prevent information disclosure via new merge request page + v 8.3.8 - Fix persistent XSS vulnerability in `commit_person_link` helper @@ -652,6 +717,17 @@ v 8.3.0 - Expose Git's version in the admin area - Show "New Merge Request" buttons on canonical repos when you have a fork (Josh Frye) +v 8.2.5 + - Prevent privilege escalation via "impersonate" feature + - Prevent privilege escalation via notes API + - Prevent privilege escalation via project webhook API + - Prevent XSS via `window.opener` + - Prevent information disclosure via project labels + - Prevent information disclosure via new merge request page + +v 8.2.4 + - Bump Git version requirement to 2.7.4 + v 8.2.3 - Fix application settings cache not expiring after changes (Stan Hu) - Fix Error 500s when creating global milestones with Unicode characters (Stan Hu) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 24cd5864530..084c2d616a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,7 @@ source edition, and GitLab Enterprise Edition (EE) which is our commercial edition. Throughout this guide you will see references to CE and EE for abbreviation. -If you have read this guide and want to know how the GitLab [core team][core-team] +If you have read this guide and want to know how the GitLab [core team] operates please see [the GitLab contributing process](PROCESS.md). ## Contributor license agreement @@ -135,8 +135,9 @@ For feature proposals for EE, open an issue on the In order to help track the feature proposals, we have created a [`feature proposal`][fpl] label. For the time being, users that are not members -of the project cannot add labels. You can instead ask one of the [core team][core-team] -members to add the label `feature proposal` to the issue. +of the project cannot add labels. You can instead ask one of the [core team] +members to add the label `feature proposal` to the issue or add the following +code snippet right after your description in a new line: `~"feature proposal"`. Please keep feature proposals as small and simple as possible, complex ones might be edited to make them small and simple. @@ -344,8 +345,7 @@ is it will be merged (quickly). After that you can send more MRs to enhance it. For examples of feedback on merge requests please look at already [closed merge requests][closed-merge-requests]. If you would like quick feedback on your merge request feel free to mention one of the Merge Marshalls in the -[core team][core-team] or one of the -[Merge request coaches](https://about.gitlab.com/team/). +[core team] or one of the [Merge request coaches](https://about.gitlab.com/team/). Please ensure that your merge request meets the contribution acceptance criteria. When having your code reviewed and when reviewing merge requests please take the @@ -497,7 +497,7 @@ reported by emailing `contact@gitlab.com`. This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant], version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/). -[core-team]: https://about.gitlab.com/core-team/ +[core team]: https://about.gitlab.com/core-team/ [getting-help]: https://about.gitlab.com/getting-help/ [codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq [up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=up-for-grabs @@ -19,8 +19,7 @@ gem "pg", '~> 0.18.2', group: :postgres # Authentication libraries gem 'devise', '~> 3.5.4' -gem 'devise-async', '~> 0.9.0' -gem 'doorkeeper', '~> 2.2.0' +gem 'doorkeeper', '~> 3.1' gem 'omniauth', '~> 1.3.1' gem 'omniauth-auth0', '~> 1.4.1' gem 'omniauth-azure-oauth2', '~> 0.0.6' @@ -217,7 +216,7 @@ gem 'font-awesome-rails', '~> 4.6.1' gem 'gitlab_emoji', '~> 0.3.0' gem 'gon', '~> 6.0.1' gem 'jquery-atwho-rails', '~> 1.3.2' -gem 'jquery-rails', '~> 4.0.0' +gem 'jquery-rails', '~> 4.1.0' gem 'jquery-scrollto-rails', '~> 1.4.3' gem 'jquery-ui-rails', '~> 5.0.0' gem 'raphael-rails', '~> 2.1.2' @@ -243,7 +242,7 @@ group :development do gem 'brakeman', '~> 3.2.0', require: false gem "annotate", "~> 2.7.0" - gem "letter_opener", '~> 1.1.2' + gem 'letter_opener_web', '~> 1.3.0' gem 'quiet_assets', '~> 1.0.2' gem 'rerun', '~> 0.11.0' gem 'bullet', require: false diff --git a/Gemfile.lock b/Gemfile.lock index dcb59b5499f..350aa7d3202 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -164,8 +164,6 @@ GEM responders thread_safe (~> 0.1) warden (~> 1.2.3) - devise-async (0.9.0) - devise (~> 3.2) devise-two-factor (2.0.1) activesupport attr_encrypted (~> 1.3.2) @@ -175,7 +173,7 @@ GEM diff-lcs (1.2.5) diffy (3.0.7) docile (1.1.5) - doorkeeper (2.2.2) + doorkeeper (3.1.0) railties (>= 3.2) dropzonejs-rails (0.7.2) rails (> 3.1) @@ -186,7 +184,7 @@ GEM encryptor (1.3.0) equalizer (0.0.11) erubis (2.7.0) - escape_utils (1.1.0) + escape_utils (1.1.1) eventmachine (1.0.8) excon (0.45.4) execjs (2.6.0) @@ -336,7 +334,7 @@ GEM json get_process_mem (0.2.0) gherkin-ruby (0.3.2) - github-linguist (4.7.5) + github-linguist (4.7.6) charlock_holmes (~> 0.7.3) escape_utils (~> 1.1.0) mime-types (>= 1.19) @@ -346,14 +344,14 @@ GEM flowdock (~> 0.7) gitlab-grit (>= 2.4.1) multi_json - gitlab-grit (2.7.3) + gitlab-grit (2.8.1) charlock_holmes (~> 0.6) diff-lcs (~> 1.1) - mime-types (~> 1.15) + mime-types (>= 1.16, < 3) posix-spawn (~> 0.3) gitlab_emoji (0.3.1) gemojione (~> 2.2, >= 2.2.1) - gitlab_git (10.0.0) + gitlab_git (10.0.1) activesupport (~> 4.0) charlock_holmes (~> 0.7.3) github-linguist (~> 4.7.0) @@ -431,8 +429,8 @@ GEM json ipaddress (0.8.2) jquery-atwho-rails (1.3.2) - jquery-rails (4.0.5) - rails-dom-testing (~> 1.0) + jquery-rails (4.1.1) + rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) jquery-scrollto-rails (1.4.3) @@ -450,8 +448,12 @@ GEM kgio (2.10.0) launchy (2.4.3) addressable (~> 2.3) - letter_opener (1.1.2) + letter_opener (1.4.1) launchy (~> 2.2) + letter_opener_web (1.3.0) + actionmailer (>= 3.2) + letter_opener (~> 1.0) + railties (>= 3.2) licensee (8.0.0) rugged (>= 0.24b) listen (3.0.5) @@ -465,7 +467,7 @@ GEM mime-types (>= 1.16, < 4) mail_room (0.6.1) method_source (0.8.2) - mime-types (1.25.1) + mime-types (2.99.1) mimemagic (0.3.0) mini_portile2 (2.0.0) minitest (5.7.0) @@ -918,10 +920,9 @@ DEPENDENCIES database_cleaner (~> 1.4.0) default_value_for (~> 3.0.0) devise (~> 3.5.4) - devise-async (~> 0.9.0) devise-two-factor (~> 2.0.0) diffy (~> 3.0.3) - doorkeeper (~> 2.2.0) + doorkeeper (~> 3.1) dropzonejs-rails (~> 0.7.1) email_reply_parser (~> 0.5.8) email_spec (~> 1.6.0) @@ -952,12 +953,12 @@ DEPENDENCIES httparty (~> 0.13.3) influxdb (~> 0.2) jquery-atwho-rails (~> 1.3.2) - jquery-rails (~> 4.0.0) + jquery-rails (~> 4.1.0) jquery-scrollto-rails (~> 1.4.3) jquery-turbolinks (~> 2.1.0) jquery-ui-rails (~> 5.0.0) kaminari (~> 0.16.3) - letter_opener (~> 1.1.2) + letter_opener_web (~> 1.3.0) licensee (~> 8.0.0) loofah (~> 2.0.3) mail_room (~> 0.6.1) diff --git a/PROCESS.md b/PROCESS.md index e34f59c6bce..fe3a963110d 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -59,7 +59,7 @@ core team members will mention this person. Workflow labels are purposely not very detailed since that would be hard to keep updated as you would need to re-evaluate them after every comment. We optionally -use functional labels on demand when want to group related issues to get an +use functional labels on demand when we want to group related issues to get an overview (for example all issues related to RVM, to tackle them in one go) and to add details to the issue. @@ -73,6 +73,7 @@ in support or comment for further detail. Do not use `feature request`. - ~bug is an issue reporting undesirable or incorrect behavior. - ~customer is an issue reported by enterprise subscribers. This label should be accompanied by *bug* or *feature proposal* labels. + Example workflow: when a UX designer provided a design but it needs frontend work they remove the UX label and add the frontend label. ## Functional labels diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index af4462ece38..bf95e06b4e5 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -1,77 +1,77 @@ class @AwardsHandler - constructor: (@get_emojis_url, @post_emoji_url, @noteable_type, @noteable_id, @aliases) -> - $(".js-add-award").on "click", (event) => + constructor: (@getEmojisUrl, @postEmojiUrl, @noteableType, @noteableId, @unicodes) -> + $('.js-add-award').on 'click', (event) => event.stopPropagation() event.preventDefault() @showEmojiMenu() - $("html").on 'click', (event) -> - if !$(event.target).closest(".emoji-menu").length - if $(".emoji-menu").is(":visible") - $(".emoji-menu").removeClass "is-visible" + $('html').on 'click', (event) -> + if !$(event.target).closest('.emoji-menu').length + if $('.emoji-menu').is(':visible') + $('.emoji-menu').removeClass 'is-visible' - $(".awards") - .off "click" - .on "click", ".js-emoji-btn", @handleClick + $('.awards') + .off 'click' + .on 'click', '.js-emoji-btn', @handleClick @renderFrequentlyUsedBlock() handleClick: (e) -> e.preventDefault() emoji = $(this) - .find(".icon") - .data "emoji" + .find('.icon') + .data 'emoji' - if emoji is "thumbsup" and awards_handler.didUserClickEmoji $(this), "thumbsdown" - awards_handler.addAward "thumbsdown" + if emoji is 'thumbsup' and awardsHandler.didUserClickEmoji $(this), 'thumbsdown' + awardsHandler.addAward 'thumbsdown' - else if emoji is "thumbsdown" and awards_handler.didUserClickEmoji $(this), "thumbsup" - awards_handler.addAward "thumbsup" + else if emoji is 'thumbsdown' and awardsHandler.didUserClickEmoji $(this), 'thumbsup' + awardsHandler.addAward 'thumbsup' - awards_handler.addAward emoji + awardsHandler.addAward emoji + + $(this).trigger 'blur' didUserClickEmoji: (that, emoji) -> - if $(that).siblings("button:has([data-emoji=#{emoji}])").attr("data-original-title") - $(that).siblings("button:has([data-emoji=#{emoji}])").attr("data-original-title").indexOf('me') > -1 + if $(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title') + $(that).siblings("button:has([data-emoji=#{emoji}])").attr('data-original-title').indexOf('me') > -1 showEmojiMenu: -> - if $(".emoji-menu").length - if $(".emoji-menu").is ".is-visible" - $(".emoji-menu").removeClass "is-visible" - $("#emoji_search").blur() + if $('.emoji-menu').length + if $('.emoji-menu').is '.is-visible' + $('.emoji-menu').removeClass 'is-visible' + $('#emoji_search').blur() else - $(".emoji-menu").addClass "is-visible" - $("#emoji_search").focus() + $('.emoji-menu').addClass 'is-visible' + $('#emoji_search').focus() else - $('.js-add-award').addClass "is-loading" - $.get @get_emojis_url, (response) => - $('.js-add-award').removeClass "is-loading" - $(".js-award-holder").append response + $('.js-add-award').addClass 'is-loading' + $.get @getEmojisUrl, (response) => + $('.js-add-award').removeClass 'is-loading' + $('.js-award-holder').append response setTimeout => - $(".emoji-menu").addClass "is-visible" - $("#emoji_search").focus() + $('.emoji-menu').addClass 'is-visible' + $('#emoji_search').focus() @setupSearch() , 200 addAward: (emoji) -> - emoji = @normilizeEmojiName(emoji) @postEmoji emoji, => @addAwardToEmojiBar(emoji) - $(".emoji-menu").removeClass "is-visible" + $('.emoji-menu').removeClass 'is-visible' addAwardToEmojiBar: (emoji) -> @addEmojiToFrequentlyUsedList(emoji) - emoji = @normilizeEmojiName(emoji) if @exist(emoji) if @isActive(emoji) @decrementCounter(emoji) else - counter = @findEmojiIcon(emoji).siblings(".js-counter") + counter = @findEmojiIcon(emoji).siblings('.js-counter') counter.text(parseInt(counter.text()) + 1) - counter.parent().addClass("active") + counter.parent().addClass('active') @addMeToAuthorList(emoji) else @createEmoji(emoji) @@ -80,47 +80,47 @@ class @AwardsHandler @findEmojiIcon(emoji).length > 0 isActive: (emoji) -> - @findEmojiIcon(emoji).parent().hasClass("active") + @findEmojiIcon(emoji).parent().hasClass('active') decrementCounter: (emoji) -> - counter = @findEmojiIcon(emoji).siblings(".js-counter") + counter = @findEmojiIcon(emoji).siblings('.js-counter') emojiIcon = counter.parent() if parseInt(counter.text()) > 1 counter.text(parseInt(counter.text()) - 1) - emojiIcon.removeClass("active") + emojiIcon.removeClass('active') @removeMeFromAuthorList(emoji) - else if emoji == "thumbsup" || emoji == "thumbsdown" - emojiIcon.tooltip("destroy") + else if emoji == 'thumbsup' || emoji == 'thumbsdown' + emojiIcon.tooltip('destroy') counter.text(0) - emojiIcon.removeClass("active") + emojiIcon.removeClass('active') @removeMeFromAuthorList(emoji) else - emojiIcon.tooltip("destroy") + emojiIcon.tooltip('destroy') emojiIcon.remove() removeMeFromAuthorList: (emoji) -> - award_block = @findEmojiIcon(emoji).parent() - authors = award_block - .attr("data-original-title") - .split(", ") - authors.splice(authors.indexOf("me"),1) - award_block - .closest(".js-emoji-btn") - .attr("data-original-title", authors.join(", ")) - @resetTooltip(award_block) + awardBlock = @findEmojiIcon(emoji).parent() + authors = awardBlock + .attr('data-original-title') + .split(', ') + authors.splice(authors.indexOf('me'),1) + awardBlock + .closest('.js-emoji-btn') + .attr('data-original-title', authors.join(', ')) + @resetTooltip(awardBlock) addMeToAuthorList: (emoji) -> - award_block = @findEmojiIcon(emoji).parent() - origTitle = award_block.attr("data-original-title").trim() + awardBlock = @findEmojiIcon(emoji).parent() + origTitle = awardBlock.attr('data-original-title').trim() authors = [] if origTitle authors = origTitle.split(', ') - authors.push("me") - award_block.attr("data-original-title", authors.join(", ")) - @resetTooltip(award_block) + authors.push('me') + awardBlock.attr('data-original-title', authors.join(', ')) + @resetTooltip(awardBlock) resetTooltip: (award) -> - award.tooltip("destroy") + award.tooltip('destroy') # "destroy" call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout. setTimeout (-> @@ -139,28 +139,28 @@ class @AwardsHandler "</button>" ) - emoji_node = $(nodes.join("\n")) - .insertBefore(".js-award-holder") - .find(".emoji-icon") - .data("emoji", emoji) + $(nodes.join("\n")) + .insertBefore('.js-award-holder') + .find('.emoji-icon') + .data('emoji', emoji) $('.award-control').tooltip() resolveNameToCssClass: (emoji) -> - emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']") + emojiIcon = $(".emoji-menu-content [data-emoji='#{emoji}']") - if emoji_icon.length > 0 - unicodeName = emoji_icon.data("unicode-name") + if emojiIcon.length > 0 + unicodeName = emojiIcon.data('unicode-name') else # Find by alias - unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data("unicode-name") + unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data('unicode-name') "emoji-#{unicodeName}" postEmoji: (emoji, callback) -> - $.post @post_emoji_url, { note: { + $.post @postEmojiUrl, { note: { note: ":#{emoji}:" - noteable_type: @noteable_type - noteable_id: @noteable_id + noteable_type: @noteableType + noteable_id: @noteableId }},(data) -> if data.ok callback.call() @@ -173,46 +173,43 @@ class @AwardsHandler scrollTop: $('.awards').offset().top - 80 }, 200) - normilizeEmojiName: (emoji) -> - @aliases[emoji] || emoji - addEmojiToFrequentlyUsedList: (emoji) -> - frequently_used_emojis = @getFrequentlyUsedEmojis() - frequently_used_emojis.push(emoji) - $.cookie('frequently_used_emojis', frequently_used_emojis.join(","), { expires: 365 }) + frequentlyUsedEmojis = @getFrequentlyUsedEmojis() + frequentlyUsedEmojis.push(emoji) + $.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 }) getFrequentlyUsedEmojis: -> - frequently_used_emojis = ($.cookie('frequently_used_emojis') || "").split(",") - _.compact(_.uniq(frequently_used_emojis)) + frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') || '').split(',') + _.compact(_.uniq(frequentlyUsedEmojis)) renderFrequentlyUsedBlock: -> if $.cookie('frequently_used_emojis') - frequently_used_emojis = @getFrequentlyUsedEmojis() + frequentlyUsedEmojis = @getFrequentlyUsedEmojis() - ul = $("<ul>") + ul = $('<ul>') - for emoji in frequently_used_emojis + for emoji in frequentlyUsedEmojis do (emoji) -> - $(".emoji-menu-content [data-emoji='#{emoji}']").closest("li").clone().appendTo(ul) + $(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul) - $("input.emoji-search").after(ul).after($("<h5>").text("Frequently used")) + $('input.emoji-search').after(ul).after($('<h5>').text('Frequently used')) setupSearch: -> - $("input.emoji-search").keyup (ev) => + $('input.emoji-search').keyup (ev) => term = $(ev.target).val() # Clean previous search results - $("ul.emoji-menu-search, h5.emoji-search").remove() + $('ul.emoji-menu-search, h5.emoji-search').remove() if term # Generate a search result block - h5 = $("<h5>").text("Search results").addClass("emoji-search") - found_emojis = @searchEmojis(term).show() - ul = $("<ul>").addClass("emoji-menu-list emoji-menu-search").append(found_emojis) - $(".emoji-menu-content ul, .emoji-menu-content h5").hide() - $(".emoji-menu-content").append(h5).append(ul) + h5 = $('<h5>').text('Search results').addClass('emoji-search') + foundEmojis = @searchEmojis(term).show() + ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis) + $('.emoji-menu-content ul, .emoji-menu-content h5').hide() + $('.emoji-menu-content').append(h5).append(ul) else - $(".emoji-menu-content").children().show() + $('.emoji-menu-content').children().show() searchEmojis: (term)-> $(".emoji-menu-content [data-emoji*='#{term}']").closest("li").clone() diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 2fdb7562515..f91aa3c5ad7 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -108,6 +108,8 @@ class Dispatcher new BuildArtifacts() when 'projects:group_links:index' new GroupsSelect() + when 'search:show' + new Search() switch path.first() when 'admin' diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 29466e9f2ed..1d1bfeb2e77 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -184,6 +184,9 @@ class GitLabDropdown @dropdown.on "shown.bs.dropdown", @opened @dropdown.on "hidden.bs.dropdown", @hidden @dropdown.on "click", ".dropdown-menu, .dropdown-menu-close", @shouldPropagate + @dropdown.on 'keyup', (e) => + if e.which is 27 # Escape key + $('.dropdown-menu-close', @dropdown).trigger 'click' if @dropdown.find(".dropdown-toggle-page").length @dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) => diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee index 3a439b94c59..3c491ebfc4c 100644 --- a/app/assets/javascripts/issuable_context.js.coffee +++ b/app/assets/javascripts/issuable_context.js.coffee @@ -10,8 +10,8 @@ class @IssuableContext $(this).submit() $(document) - .off 'click', '.dropdown-content a' - .on 'click', '.dropdown-content a', (e) -> + .off 'click', '.issuable-sidebar .dropdown-content a' + .on 'click', '.issuable-sidebar .dropdown-content a', (e) -> e.preventDefault() $(document) diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee index c7d74a12f99..157361404e0 100644 --- a/app/assets/javascripts/issue.js.coffee +++ b/app/assets/javascripts/issue.js.coffee @@ -12,6 +12,7 @@ class @Issue @initMergeRequests() @initRelatedBranches() + @initCanCreateBranch() initTaskList: -> $('.detail-page-description .js-task-list-container').taskList('enable') @@ -92,3 +93,25 @@ class @Issue .success (data) -> if 'html' of data $container.html(data.html) + + initCanCreateBranch: -> + $container = $('div#new-branch') + + # If the user doesn't have the required permissions the container isn't + # rendered at all. + return unless $container + + $.getJSON($container.data('path')) + .error -> + $container.find('.checking').hide() + $container.find('.unavailable').show() + + new Flash('Failed to check if a new branch can be created.', 'alert') + .success (data) -> + if data.can_create_branch + $container.find('.checking').hide() + $container.find('.available').show() + $container.find('a').attr('disabled', false) + else + $container.find('.checking').hide() + $container.find('.unavailable').show() diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 85517b18c5a..995fd768603 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -30,7 +30,7 @@ class @LabelsSelect if issueUpdateURL labelHTMLTemplate = _.template( '<% _.each(labels, function(label){ %> - <a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= _.escape(label.title) %>"> + <a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%= _.escape(label.title) %>"> <span class="label has-tooltip color-label" title="<%= _.escape(label.description) %>" style="background-color: <%= label.color %>; color: <%= label.text_color %>;"> <%= _.escape(label.title) %> </span> @@ -163,6 +163,21 @@ class @LabelsSelect $.ajax( url: labelUrl ).done (data) -> + data = _.chain data + .groupBy (label) -> + label.title + .map (label) -> + color = _.map label, (dup) -> + dup.color + + return { + id: label[0].id + title: label[0].title + color: color + duplicate: color.length > 1 + } + .value() + if $dropdown.hasClass 'js-extra-options' if showNo data.unshift( @@ -178,6 +193,7 @@ class @LabelsSelect if data.length > 2 data.splice 2, 0, 'divider' + callback data renderRow: (label) -> @@ -192,11 +208,31 @@ class @LabelsSelect if $dropdown.hasClass('js-multiselect') and removesAll selectedClass.push 'dropdown-clear-active' - color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else "" + if label.duplicate + spacing = 100 / label.color.length + + # Reduce the colors to 4 + label.color = label.color.filter (color, i) -> + i < 4 + + color = _.map(label.color, (color, i) -> + percentFirst = Math.floor(spacing * i) + percentSecond = Math.floor(spacing * (i + 1)) + "#{color} #{percentFirst}%,#{color} #{percentSecond}% " + ).join(',') + color = "linear-gradient(#{color})" + else + if label.color? + color = label.color[0] + + if color + colorEl = "<span class='dropdown-label-box' style='background: #{color}'></span>" + else + colorEl = '' "<li> <a href='#' class='#{selectedClass.join(' ')}'> - #{color} + #{colorEl} #{_.escape(label.title)} </a> </li>" diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 82e210fed7d..efb3e8e2198 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -167,8 +167,8 @@ class @Notes return if note.award - awards_handler.addAwardToEmojiBar(note.note) - awards_handler.scrollToAwards() + awardsHandler.addAwardToEmojiBar(note.note) + awardsHandler.scrollToAwards() # render note if it not present in loaded list # or skip if rendered @@ -373,11 +373,11 @@ class @Notes new GLForm form if scrollTo? and myLastNote? - # scroll to the bottom + # scroll to the bottom # so the open of the last element doesn't make a jump $('html, body').scrollTop($(document).height()); $('html, body').animate({ - scrollTop: myLastNote.offset().top - 150 + scrollTop: myLastNote.offset().top - 150 }, 500, -> $noteText = form.find(".js-note-text") $noteText.focus() diff --git a/app/assets/javascripts/right_sidebar.js.coffee b/app/assets/javascripts/right_sidebar.js.coffee index 67403554340..2d084b76cfe 100644 --- a/app/assets/javascripts/right_sidebar.js.coffee +++ b/app/assets/javascripts/right_sidebar.js.coffee @@ -1,10 +1,12 @@ class @Sidebar constructor: (currentUser) -> + @sidebar = $('aside') + @addEventListeners() addEventListeners: -> - $('aside').on('click', '.sidebar-collapsed-icon', @sidebarCollapseClicked) - $('.dropdown').on('hidden.gl.dropdown', @sidebarDropdownHidden) + @sidebar.on('click', '.sidebar-collapsed-icon', @, @sidebarCollapseClicked) + $('.dropdown').on('hidden.gl.dropdown', @, @onSidebarDropdownHidden) $('.dropdown').on('loading.gl.dropdown', @sidebarDropdownLoading) $('.dropdown').on('loaded.gl.dropdown', @sidebarDropdownLoaded) @@ -30,26 +32,56 @@ class @Sidebar else i.show() - sidebarCollapseClicked: (e) -> + sidebar = e.data e.preventDefault() $block = $(@).closest('.block') + sidebar.openDropdown($block); - $('aside') - .find('.gutter-toggle') - .trigger('click') - $editLink = $block.find('.edit-link') + openDropdown: (blockOrName) -> + $block = if _.isString(blockOrName) then @getBlock(blockOrName) else blockOrName + + $block.find('.edit-link').trigger('click') - if $editLink.length - $editLink.trigger('click') - $block.addClass('collapse-after-update') - $('.page-with-sidebar').addClass('with-overlay') + if not @isOpen() + @setCollapseAfterUpdate($block) + @toggleSidebar('open') - sidebarDropdownHidden: (e) -> + setCollapseAfterUpdate: ($block) -> + $block.addClass('collapse-after-update') + $('.page-with-sidebar').addClass('with-overlay') + + onSidebarDropdownHidden: (e) -> + sidebar = e.data + e.preventDefault() $block = $(@).closest('.block') + sidebar.sidebarDropdownHidden($block) + + sidebarDropdownHidden: ($block) -> if $block.hasClass('collapse-after-update') $block.removeClass('collapse-after-update') $('.page-with-sidebar').removeClass('with-overlay') - $('aside') - .find('.gutter-toggle') - .trigger('click')
\ No newline at end of file + @toggleSidebar('hide') + + triggerOpenSidebar: -> + @sidebar + .find('.js-sidebar-toggle') + .trigger('click') + + toggleSidebar: (action = 'toggle') -> + if action is 'toggle' + @triggerOpenSidebar() + + if action is 'open' + @triggerOpenSidebar() if not @isOpen() + + if action is 'hide' + @triggerOpenSidebar() is @isOpen() + + isOpen: -> + @sidebar.is('.right-sidebar-expanded') + + getBlock: (name) -> + @sidebar.find(".block.#{name}") + + diff --git a/app/assets/javascripts/search.js.coffee b/app/assets/javascripts/search.js.coffee new file mode 100644 index 00000000000..661e1195f60 --- /dev/null +++ b/app/assets/javascripts/search.js.coffee @@ -0,0 +1,75 @@ +class @Search + constructor: -> + $groupDropdown = $('.js-search-group-dropdown') + $projectDropdown = $('.js-search-project-dropdown') + @eventListeners() + + $groupDropdown.glDropdown( + selectable: true + filterable: true + fieldName: 'group_id' + data: (term, callback) -> + Api.groups term, null, (data) -> + data.unshift( + name: 'Any' + ) + data.splice 1, 0, 'divider' + + callback(data) + id: (obj) -> + obj.id + text: (obj) -> + obj.name + toggleLabel: (obj) -> + "#{$groupDropdown.data('default-label')} #{obj.name}" + clicked: => + @submitSearch() + ) + + $projectDropdown.glDropdown( + selectable: true + filterable: true + fieldName: 'project_id' + data: (term, callback) -> + Api.projects term, 'id', (data) -> + data.unshift( + name_with_namespace: 'Any' + ) + data.splice 1, 0, 'divider' + + callback(data) + id: (obj) -> + obj.id + text: (obj) -> + obj.name_with_namespace + toggleLabel: (obj) -> + "#{$projectDropdown.data('default-label')} #{obj.name_with_namespace}" + clicked: => + @submitSearch() + ) + + eventListeners: -> + $(document) + .off 'keyup', '.js-search-input' + .on 'keyup', '.js-search-input', @searchKeyUp + + $(document) + .off 'click', '.js-search-clear' + .on 'click', '.js-search-clear', @clearSearchField + + submitSearch: -> + $('.js-search-form').submit() + + searchKeyUp: -> + $input = $(@) + + if $input.val() is '' + $('.js-search-clear').addClass 'hidden' + else + $('.js-search-clear').removeClass 'hidden' + + clearSearchField: -> + $('.js-search-input') + .val '' + .trigger 'keyup' + .focus() diff --git a/app/assets/javascripts/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee index 100e3aac535..f3d66004138 100644 --- a/app/assets/javascripts/shortcuts.js.coffee +++ b/app/assets/javascripts/shortcuts.js.coffee @@ -2,34 +2,35 @@ class @Shortcuts constructor: -> @enabledHelp = [] Mousetrap.reset() - Mousetrap.bind('?', @selectiveHelp) + Mousetrap.bind('?', @onToggleHelp) Mousetrap.bind('s', Shortcuts.focusSearch) Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview) Mousetrap.bind('t', -> Turbolinks.visit(findFileURL)) if findFileURL? - selectiveHelp: (e) => - Shortcuts.showHelp(e, @enabledHelp) + onToggleHelp: (e) => + e.preventDefault() + @toggleHelp(@enabledHelp) toggleMarkdownPreview: (e) => $(document).triggerHandler('markdown-preview:toggle', [e]) - @showHelp: (e, location) -> - if $('#modal-shortcuts').length > 0 - $('#modal-shortcuts').modal('show') - else - url = '/help/shortcuts' - url = gon.relative_url_root + url if gon.relative_url_root? - $.ajax( - url: url, - dataType: 'script', - success: (e) -> - if location and location.length > 0 - $(l).show() for l in location - else - $('.hidden-shortcut').show() - $('.js-more-help-button').remove() - ) - e.preventDefault() + toggleHelp: (location) -> + $modal = $('#modal-shortcuts') + + if $modal.length + $modal.modal('toggle') + return + + $.ajax( + url: gon.shortcuts_path, + dataType: 'script', + success: (e) -> + if location and location.length > 0 + $(l).show() for l in location + else + $('.hidden-shortcut').show() + $('.js-more-help-button').remove() + ) @focusSearch: (e) -> $('#search').focus() diff --git a/app/assets/javascripts/shortcuts_issuable.coffee b/app/assets/javascripts/shortcuts_issuable.coffee index bbf02f1db24..ad9b3c1c6bf 100644 --- a/app/assets/javascripts/shortcuts_issuable.coffee +++ b/app/assets/javascripts/shortcuts_issuable.coffee @@ -4,18 +4,8 @@ class @ShortcutsIssuable extends ShortcutsNavigation constructor: (isMergeRequest) -> super() - Mousetrap.bind('a', -> - $('.block.assignee .edit-link').trigger('click') - return false - ) - Mousetrap.bind('m', -> - $('.block.milestone .edit-link').trigger('click') - return false - ) - Mousetrap.bind('r', => - @replyWithSelectedText() - return false - ) + Mousetrap.bind('a', @openSidebarDropdown.bind(@, 'assignee')) + Mousetrap.bind('m', @openSidebarDropdown.bind(@, 'milestone')) Mousetrap.bind('j', => @prevIssue() return false @@ -28,7 +18,7 @@ class @ShortcutsIssuable extends ShortcutsNavigation @editIssue() return false ) - + Mousetrap.bind('l', @openSidebarDropdown.bind(@, 'labels')) if isMergeRequest @enabledHelp.push('.hidden-shortcut.merge_requests') @@ -71,3 +61,7 @@ class @ShortcutsIssuable extends ShortcutsNavigation editIssue: -> $editBtn = $('.issuable-edit') Turbolinks.visit($editBtn.attr('href')) + + openSidebarDropdown: (name) -> + sidebar.openDropdown(name) + return false diff --git a/app/assets/javascripts/shortcuts_navigation.coffee b/app/assets/javascripts/shortcuts_navigation.coffee index 8decaedd87b..f39504e0645 100644 --- a/app/assets/javascripts/shortcuts_navigation.coffee +++ b/app/assets/javascripts/shortcuts_navigation.coffee @@ -14,6 +14,7 @@ class @ShortcutsNavigation extends Shortcuts Mousetrap.bind('g m', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-merge_requests')) Mousetrap.bind('g w', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-wiki')) Mousetrap.bind('g s', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-snippets')) + Mousetrap.bind('i', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-new-issue')) @enabledHelp.push('.hidden-shortcut.project') @findAndFollowLink: (selector) -> diff --git a/app/assets/javascripts/user_tabs.js.coffee b/app/assets/javascripts/user_tabs.js.coffee index 09b7eec9104..c2aeffe2381 100644 --- a/app/assets/javascripts/user_tabs.js.coffee +++ b/app/assets/javascripts/user_tabs.js.coffee @@ -92,7 +92,7 @@ class @UserTabs @setCurrentAction(action) activateTab: (action) -> - @parentEl.find(".nav-links .#{action}-tab a").tab('show') + @parentEl.find(".nav-links .js-#{action}-tab a").tab('show') setTab: (source, action) -> return if @loaded[action] is true diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index c85ab9148d0..560de9fc0bd 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -25,6 +25,7 @@ @import "framework/lists.scss"; @import "framework/markdown_area.scss"; @import "framework/mobile.scss"; +@import "framework/modal.scss"; @import "framework/nav.scss"; @import "framework/pagination.scss"; @import "framework/progress.scss"; diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 62b2af0dbf7..e72e4aa47ef 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -1,5 +1,5 @@ .light-well { - background-color: #f8fafc; + background-color: $background-color; padding: 15px; } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 18a74fe21a0..062da397b6b 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -139,6 +139,10 @@ pointer-events: auto !important; } + &[disabled] { + pointer-events: none !important; + } + .caret { margin-left: 5px; } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 2ade341c9dd..3386523dbf7 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -11,6 +11,7 @@ .prepend-top-10 { margin-top: 10px } .prepend-top-default { margin-top: $gl-padding !important; } .prepend-top-20 { margin-top: 20px } +.prepend-left-5 { margin-left: 5px } .prepend-left-10 { margin-left: 10px } .prepend-left-default { margin-left: $gl-padding; } .prepend-left-20 { margin-left: 20px } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 239eaf15cc1..4bf3a050403 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -42,7 +42,7 @@ font-size: 15px; text-align: left; border: 1px solid $dropdown-toggle-border-color; - border-radius: $dropdown-border-radius; + border-radius: $border-radius-base; outline: 0; text-overflow: ellipsis; white-space: nowrap; @@ -80,7 +80,7 @@ padding: 10px 0; background-color: $dropdown-bg; border: 1px solid $dropdown-border-color; - border-radius: $dropdown-border-radius; + border-radius: $border-radius-base; box-shadow: 0 2px 4px $dropdown-shadow-color; &.is-loading { diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 54cb5461113..558b133f593 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -78,6 +78,24 @@ label { border-radius: 3px; } +.select-wrapper { + position: relative; + + .caret { + position: absolute; + right: 10px; + top: $gl-padding; + color: $gray-darkest; + pointer-events: none; + } +} + +.select-control { + padding-left: 10px; + padding-right: 10px; + -webkit-appearance: none; +} + .form-control-inline { display: inline; } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index c303380764b..5fa10d29a87 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -26,9 +26,9 @@ header { z-index: 100; margin-bottom: 0; min-height: $header-height; - background-color: #fff; + background-color: $background-color; border: none; - border-bottom: 1px solid #eee; + border-bottom: 1px solid $border-color; .container-fluid { width: 100% !important; @@ -47,7 +47,7 @@ header { text-align: center; &:hover, &:focus, &:active { - background-color: #fff; + background-color: $background-color; } } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 704fa1ff800..fd885b38680 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -95,7 +95,7 @@ &.md-preview-holder { code { white-space: pre-wrap; - word-break: break-all; + word-break: keep-all; } } } diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss new file mode 100644 index 00000000000..26ad2870aa0 --- /dev/null +++ b/app/assets/stylesheets/framework/modal.scss @@ -0,0 +1,22 @@ +.modal-body { + position: relative; + overflow-y: auto; + padding: 15px; + + .form-actions { + margin: -$gl-padding+1; + margin-top: 15px; + } + + .text-danger { + font-weight: bold; + } +} + +body.modal-open { + overflow: hidden; +} + +.modal .modal-dialog { + width: 860px; +} diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 192d53b048a..5fe687dcec3 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -185,3 +185,22 @@ } } } + +.layout-nav { + background: $background-color; + border-bottom: 1px solid $border-color; + + .controls { + float: right; + position: relative; + top: 10px; + + .dropdown { + margin-left: 7px; + } + } + + .nav-links { + border-bottom: none; + } +} diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index eae5f062dda..6efc6ec1e4b 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -7,13 +7,11 @@ .select2-choice { background: #fff; border-color: $input-border; - border-color: $border-white-light; height: 35px; padding: $gl-vert-padding $gl-btn-padding; font-size: $gl-font-size; line-height: 1.42857143; - - @include border-radius($border-radius-default); + border-radius: $border-radius-base; .select2-arrow { background-image: none; @@ -199,6 +197,14 @@ } } +.select2-highlighted { + .group-result { + .group-path { + color: #fff; + } + } +} + .group-result { .group-image { float: left; diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index f0ec250de2b..29501069d27 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -11,7 +11,7 @@ border-bottom: 1px solid $border-white-light; &:target { - background: $row-hover; + background: $line-target-blue; } .avatar { diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index c72af5dad0a..371c1bf17e1 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -153,8 +153,8 @@ $nav-link-padding: 13px $gl-padding; //== Code // //## -$pre-bg: #f8fafc !default; +$pre-bg: $background-color !default; $pre-color: $gl-gray !default; -$pre-border-color: #e7e9ed; +$pre-border-color: $border-color; $table-bg-accent: $background-color; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 0a5b4b8834c..b2535ddf4bd 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -205,6 +205,10 @@ h1, h2, h3, h4, h5, h6 { font-weight: 600; } +.light-header { + font-weight: 600; +} + /** CODE **/ pre { font-family: $monospace_font; @@ -259,3 +263,9 @@ h1, h2, h3, h4 { color: $gl-gray; } } + +.text-right-lg { + @media (min-width: $screen-lg-min) { + text-align: right; + } +} diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 30ca27ab104..b8ed7e8a74c 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -71,8 +71,7 @@ $gl-avatar-size: 40px; $error-exclamation-point: #e62958; $border-radius-default: 2px; $btn-transparent-color: #8f8f8f; -$ssh-key-icon-color: #8f8f8f; -$ssh-key-icon-size: 18px; +$settings-icon-size: 18px; $provider-btn-group-border: #e5e5e5; $provider-btn-not-active-color: #4688f1; @@ -168,8 +167,12 @@ $line-removed: #fbe9eb; $line-removed-dark: #fac5cd; $line-number-old: #f9d7dc; $line-number-new: #ddfbe6; +$line-number-select: #fbf2da; $match-line: #fafafa; $table-border-gray: #f0f0f0; +$line-target-blue: #eaf3fc; +$line-select-yellow: #fcf8e7; +$line-select-yellow-dark: #f0e2bd; /* * Fonts */ @@ -179,7 +182,6 @@ $regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif /* * Dropdowns */ -$dropdown-border-radius: 2px; $dropdown-width: 300px; $dropdown-bg: #fff; $dropdown-link-color: #555; diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 1ff6ad75e07..31a4e3deaac 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -21,11 +21,6 @@ // Diff line .line_holder { - td.diff-line-num.hll:not(.empty-cell), - td.line_content.hll:not(.empty-cell) { - background-color: #f8eec7; - border-color: darken(#f8eec7, 15%); - } .diff-line-num { &.old { @@ -37,11 +32,16 @@ background-color: $line-number-new; border-color: $line-added-dark; } + + &.hll:not(.empty-cell) { + background-color: $line-number-select; + border-color: $line-select-yellow-dark; + } } .line_content { &.old { - background: $line-removed; + background-color: $line-removed; span.idiff { background-color: $line-removed-dark; @@ -58,7 +58,11 @@ &.match { color: $black-transparent; - background: $match-line; + background-color: $match-line; + } + + &.hll:not(.empty-cell) { + background-color: $line-select-yellow; } } } diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 751a5ab4d92..3438dbe4958 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -40,6 +40,7 @@ .wiki { code { white-space: pre-wrap; + word-break: keep-all; } } } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index e7c8198ba45..1a7d5f9666e 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -98,7 +98,11 @@ } td.line_content.parallel { - width: 50%; + width: 46%; + } + + .add-diff-note { + margin-left: -65px; } } @@ -127,8 +131,13 @@ margin: 0; padding: 0 0.5em; border: none; + &.parallel { display: table-cell; + + span { + word-break: break-all; + } } } diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index ee95bdf488e..4a95b7b852e 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -55,25 +55,6 @@ } } -.modal-body { - position: relative; - overflow-y: auto; - padding: 15px; - - .form-actions { - margin: -$gl-padding+1; - margin-top: 15px; - } -} - -body.modal-open { - overflow: hidden; -} - -.modal .modal-dialog { - width: 860px; -} - .documentation { padding: 7px; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index d54abe9bc02..9619d65db85 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -109,6 +109,10 @@ ul.notes { border-color: darken(#f5f5f5, 8%); margin: 10px 0; } + + code { + word-break: keep-all; + } } a { @@ -211,7 +215,7 @@ ul.notes { } .discussion-actions { - @media (max-width: $screen-sm-max) { + @media (max-width: $screen-md-max) { float: none; margin-left: 0; diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index a9656e5cae7..01f98479623 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -18,7 +18,8 @@ } .account-btn-link, -.profile-settings-sidebar a { +.profile-settings-sidebar a, +.settings-sidebar a { color: $md-link-color; } @@ -123,12 +124,6 @@ } } -.key-icon { - color: $ssh-key-icon-color; - font-size: $ssh-key-icon-size; - line-height: 42px; -} - .key-created-at { line-height: 42px; } @@ -180,14 +175,6 @@ } } -.profile-settings-message { - line-height: 32px; - color: $warning-message-color; - background-color: $warning-message-bg; - border: 1px solid $warning-message-border; - border-radius: $border-radius-base; -} - .oauth-applications { form { display: inline-block; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index fcca9d4faf5..99108e9bfc4 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -202,8 +202,31 @@ min-width: 200px; } -.deploy-project-label { - margin: 1px; +.deploy-key-content { + @media (min-width: $screen-sm-min) { + float: left; + + &:last-child { + float: right; + } + } +} + +.deploy-key-projects { + @media (min-width: $screen-sm-min) { + line-height: 42px; + } +} + +a.deploy-project-label { + padding: 5px; + margin-right: 5px; + color: $gl-gray; + background-color: $row-hover; + + &:hover { + color: $gl-link-color; + } } .vs-public { @@ -256,12 +279,6 @@ } } -table.table.protected-branches-list tr.no-border { - th, td { - border: 0; - } -} - .project-import .btn { float: left; margin-right: 10px; @@ -474,3 +491,14 @@ pre.light-well { color: #fff; } } + +.protected-branches-list { + a { + color: $gl-gray; + font-weight: 600; + + &:hover { + color: $gl-link-color; + } + } +} diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index f0f3744c6fa..2bff70c8c64 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -10,17 +10,6 @@ } } -.search-holder { - max-width: 600px; - margin: 0 auto; - margin-bottom: 20px; - - input { - border-color: #bbb; - font-weight: bold; - } -} - .search { margin-right: 10px; margin-left: 10px; @@ -159,7 +148,85 @@ &.has-location-badge { .search-input-wrap { - width: 78%; + width: 68%; } } } + +.search-holder { + @media (min-width: $screen-sm-min) { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + } + + .search-field-holder { + -webkit-flex: 1 0 auto; + -ms-flex: 1 0 auto; + flex: 1 0 auto; + position: relative; + margin-right: 0; + + @media (min-width: $screen-sm-min) { + margin-right: 5px; + } + } + + .search-icon { + position: absolute; + left: 10px; + top: 10px; + color: $gray-darkest; + pointer-events: none; + } + + .search-text-input { + padding-left: $gl-padding + 15px; + padding-right: $gl-padding + 15px; + } + + .btn-search { + width: 100%; + margin-top: 5px; + + @media (min-width: $screen-sm-min) { + width: auto; + margin-top: 0; + margin-left: 5px; + } + } + + .dropdown { + @media (min-width: $screen-sm-min) { + margin-left: 5px; + margin-right: 5px; + } + } + + .dropdown-menu-toggle { + width: 100%; + margin-top: 5px; + + @media (min-width: $screen-sm-min) { + width: 160px; + margin-top: 0; + } + } +} + +.search-clear { + position: absolute; + right: 10px; + top: 10px; + padding: 0; + color: $gray-darkest; + line-height: 0; + background: none; + border: 0; + + &:hover, + &:focus { + color: $gl-link-color; + outline: none; + } +} diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss new file mode 100644 index 00000000000..3fb70085713 --- /dev/null +++ b/app/assets/stylesheets/pages/settings.scss @@ -0,0 +1,14 @@ +.settings-list-icon { + color: $gl-placeholder-color; + font-size: $settings-icon-size; + line-height: 42px; +} + +.settings-message { + padding: 5px; + line-height: 1.3; + color: $warning-message-color; + background-color: $warning-message-bg; + border: 1px solid $warning-message-border; + border-radius: $border-radius-base; +} diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb index 9083bfb41cf..cf795d977ce 100644 --- a/app/controllers/admin/application_controller.rb +++ b/app/controllers/admin/application_controller.rb @@ -6,12 +6,6 @@ class Admin::ApplicationController < ApplicationController layout 'admin' def authenticate_admin! - return render_404 unless current_user.is_admin? - end - - def authorize_impersonator! - if session[:impersonator_id] - User.find_by!(username: session[:impersonator_id]).admin? - end + render_404 unless current_user.is_admin? end end diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb index 93c4894ea0f..4e85b6b4cf2 100644 --- a/app/controllers/admin/hooks_controller.rb +++ b/app/controllers/admin/hooks_controller.rb @@ -39,6 +39,12 @@ class Admin::HooksController < Admin::ApplicationController end def hook_params - params.require(:hook).permit(:url, :enable_ssl_verification, :push_events, :tag_push_events) + params.require(:hook).permit( + :enable_ssl_verification, + :push_events, + :tag_push_events, + :token, + :url + ) end end diff --git a/app/controllers/admin/impersonation_controller.rb b/app/controllers/admin/impersonation_controller.rb deleted file mode 100644 index bf98af78615..00000000000 --- a/app/controllers/admin/impersonation_controller.rb +++ /dev/null @@ -1,38 +0,0 @@ -class Admin::ImpersonationController < Admin::ApplicationController - skip_before_action :authenticate_admin!, only: :destroy - - before_action :user - before_action :authorize_impersonator! - - def create - if @user.blocked? - flash[:alert] = "You cannot impersonate a blocked user" - - redirect_to admin_user_path(@user) - else - session[:impersonator_id] = current_user.username - session[:impersonator_return_to] = admin_user_path(@user) - - warden.set_user(user, scope: 'user') - - flash[:alert] = "You are impersonating #{user.username}." - - redirect_to root_path - end - end - - def destroy - redirect = session[:impersonator_return_to] - - warden.set_user(user, scope: 'user') - - session[:impersonator_return_to] = nil - session[:impersonator_id] = nil - - redirect_to redirect || root_path - end - - def user - @user ||= User.find_by!(username: params[:id] || session[:impersonator_id]) - end -end diff --git a/app/controllers/admin/impersonations_controller.rb b/app/controllers/admin/impersonations_controller.rb new file mode 100644 index 00000000000..2db824c87ef --- /dev/null +++ b/app/controllers/admin/impersonations_controller.rb @@ -0,0 +1,24 @@ +class Admin::ImpersonationsController < Admin::ApplicationController + skip_before_action :authenticate_admin! + before_action :authenticate_impersonator! + + def destroy + original_user = current_user + + warden.set_user(impersonator, scope: :user) + + session[:impersonator_id] = nil + + redirect_to admin_user_path(original_user) + end + + private + + def impersonator + @impersonator ||= User.find(session[:impersonator_id]) if session[:impersonator_id] + end + + def authenticate_impersonator! + render_404 unless impersonator && impersonator.is_admin? && !impersonator.blocked? + end +end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 9abf08d0e19..b8976fa09a9 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -31,6 +31,22 @@ class Admin::UsersController < Admin::ApplicationController user end + def impersonate + if user.blocked? + flash[:alert] = "You cannot impersonate a blocked user" + + redirect_to admin_user_path(user) + else + session[:impersonator_id] = current_user.id + + warden.set_user(user, scope: :user) + + flash[:alert] = "You are now impersonating #{user.username}" + + redirect_to root_path + end + end + def block if user.block redirect_back_or_admin_user(notice: "Successfully blocked") diff --git a/app/controllers/import/gitlab_project_controller.rb b/app/controllers/import/gitlab_project_controller.rb new file mode 100644 index 00000000000..ab0da196ac1 --- /dev/null +++ b/app/controllers/import/gitlab_project_controller.rb @@ -0,0 +1,45 @@ +class Import::GitlabProjectController < Import::BaseController + before_action :verify_gitlab_project_import_enabled + before_action :gitlab_project_auth, except: :callback + + rescue_from OAuth::Error, with: :gitlab_project_unauthorized + + #TODO permissions stuff + + def callback + + redirect_to status_import_gitlab_project_url + end + + def status + @repos = client.projects + @incompatible_repos = client.incompatible_projects + + @already_added_projects = current_user.created_projects.where(import_type: "gitlab_project") + already_added_projects_names = @already_added_projects.pluck(:import_source) + + @repos.to_a.reject!{ |repo| already_added_projects_names.include? "#{repo["owner"]}/#{repo["slug"]}" } + end + + def jobs + jobs = current_user.created_projects.where(import_type: "gitlab_project").to_json(only: [:id, :import_status]) + render json: jobs + end + + def create + @file = params[:file] + + repo_owner = current_user.username + @target_namespace = params[:new_namespace].presence || repo_owner + + # namespace = get_or_create_namespace || (render and return) + + @project = Gitlab::ImportExport::ImportService.execute(archive_file: file, owner: repo_owner) + end + + private + + def verify_gitlab_project_import_enabled + render_404 unless gitlab_project_import_enabled? + end +end diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 7d09288bc80..83d5ced9be8 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -7,31 +7,24 @@ class Projects::DeployKeysController < Projects::ApplicationController layout "project_settings" def index - @enabled_keys = @project.deploy_keys - - @available_keys = accessible_keys - @enabled_keys - @available_project_keys = current_user.project_deploy_keys - @enabled_keys - @available_public_keys = DeployKey.are_public - @enabled_keys - - # Public keys that are already used by another accessible project are already - # in @available_project_keys. - @available_public_keys -= @available_project_keys + @key = DeployKey.new + set_index_vars end def new - @key = @project.deploy_keys.new - - respond_with(@key) + redirect_to namespace_project_deploy_keys_path(@project.namespace, + @project) end def create @key = DeployKey.new(deploy_key_params) + set_index_vars if @key.valid? && @project.deploy_keys << @key redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) else - render "new" + render "index" end end @@ -51,6 +44,18 @@ class Projects::DeployKeysController < Projects::ApplicationController protected + def set_index_vars + @enabled_keys ||= @project.deploy_keys + + @available_keys ||= accessible_keys - @enabled_keys + @available_project_keys ||= current_user.project_deploy_keys - @enabled_keys + @available_public_keys ||= DeployKey.are_public - @enabled_keys + + # Public keys that are already used by another accessible project are already + # in @available_project_keys. + @available_public_keys -= @available_project_keys + end + def accessible_keys @accessible_keys ||= current_user.accessible_deploy_keys end diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index 5fd4f855dec..dfa9bd259e8 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -52,8 +52,16 @@ class Projects::HooksController < Projects::ApplicationController end def hook_params - params.require(:hook).permit(:url, :push_events, :issues_events, - :merge_requests_events, :tag_push_events, :note_events, - :build_events, :enable_ssl_verification) + params.require(:hook).permit( + :build_events, + :enable_ssl_verification, + :issues_events, + :merge_requests_events, + :note_events, + :push_events, + :tag_push_events, + :token, + :url + ) end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 7d4fc361ce2..016f5dd0005 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -3,8 +3,8 @@ class Projects::IssuesController < Projects::ApplicationController include IssuableActions before_action :module_enabled - before_action :issue, - only: [:edit, :update, :show, :referenced_merge_requests, :related_branches] + before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, + :related_branches, :can_create_branch] # Allow read any issue before_action :authorize_read_issue!, only: [:show] @@ -96,6 +96,8 @@ class Projects::IssuesController < Projects::ApplicationController if params[:move_to_project_id].to_i > 0 new_project = Project.find(params[:move_to_project_id]) + return render_404 unless issue.can_move?(current_user, new_project) + move_service = Issues::MoveService.new(project, current_user) @issue = move_service.execute(@issue, new_project) end @@ -139,6 +141,18 @@ class Projects::IssuesController < Projects::ApplicationController end end + def can_create_branch + can_create = current_user && + can?(current_user, :push_code, @project) && + @issue.can_be_worked_on?(current_user) + + respond_to do |format| + format.json do + render json: { can_create_branch: can_create } + end + end + end + def bulk_update result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute redirect_back_or_default(default: { action: 'index' }, options: { notice: "#{result[:count]} issues updated" }) diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index c02bc28acef..0d6c32fabd2 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -40,10 +40,10 @@ class Projects::WikisController < Projects::ApplicationController end def update - @page = @project_wiki.find_page(params[:id]) - return render('empty') unless can?(current_user, :create_wiki, @project) + @page = @project_wiki.find_page(params[:id]) + if @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page) redirect_to( namespace_project_wiki_path(@project.namespace, @project, @page), diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index e42d2d73947..69c92d2bed2 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -8,8 +8,6 @@ class SearchController < ApplicationController def show return if params[:search].nil? || params[:search].blank? - @search_term = params[:search] - if params[:project_id].present? @project = Project.find_by(id: params[:project_id]) @project = nil unless can?(current_user, :download_code, @project) @@ -20,6 +18,8 @@ class SearchController < ApplicationController @group = nil unless can?(current_user, :read_group, @group) end + @search_term = params[:search] + @scope = params[:scope] @show_snippets = params[:snippets].eql? 'true' @@ -44,7 +44,7 @@ class SearchController < ApplicationController Search::GlobalService.new(current_user, params).execute end - @objects = @search_results.objects(@scope, params[:page]) + @search_objects = @search_results.objects(@scope, params[:page]) end def autocomplete diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index a41172816b8..01cbf91c658 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -51,7 +51,7 @@ class SnippetsFinder snippets = project.snippets.fresh if current_user - if project.team.member?(current_user.id) + if project.team.member?(current_user.id) || current_user.admin? snippets else snippets.public_and_internal diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index a4d7c425d0f..474c6f27374 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -3,8 +3,8 @@ module BlobHelper Gitlab::Highlight.new(blob_name, blob_content, nowrap: nowrap) end - def highlight(blob_name, blob_content, nowrap: false) - Gitlab::Highlight.highlight(blob_name, blob_content, nowrap: nowrap) + def highlight(blob_name, blob_content, nowrap: false, plain: false) + Gitlab::Highlight.highlight(blob_name, blob_content, nowrap: nowrap, plain: plain) end def no_highlight_files diff --git a/app/helpers/ci_badge_helper.rb b/app/helpers/ci_badge_helper.rb deleted file mode 100644 index 27386133e36..00000000000 --- a/app/helpers/ci_badge_helper.rb +++ /dev/null @@ -1,13 +0,0 @@ -module CiBadgeHelper - def markdown_badge_code(project, ref) - url = status_ci_project_url(project, ref: ref, format: 'png') - link = namespace_project_commits_path(project.namespace, project, ref) - "[![build status](#{url})](#{link})" - end - - def html_badge_code(project, ref) - url = status_ci_project_url(project, ref: ref, format: 'png') - link = namespace_project_commits_path(project.namespace, project, ref) - "<a href='#{link}'><img src='#{url}' /></a>" - end -end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 97466d532f4..9f73edb4553 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -23,7 +23,7 @@ module DiffHelper end def diff_options - options = { ignore_whitespace_change: params[:w] == '1' } + options = { ignore_whitespace_change: hide_whitespace? } if diff_hard_limit_enabled? options.merge!(Commit.max_diff_options) end @@ -128,4 +128,31 @@ module DiffHelper title end end + + def commit_diff_whitespace_link(project, commit, options) + url = namespace_project_commit_path(project.namespace, project, commit.id, params_with_whitespace) + toggle_whitespace_link(url, options) + end + + def diff_merge_request_whitespace_link(project, merge_request, options) + url = diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, params_with_whitespace) + toggle_whitespace_link(url, options) + end + + private + + def hide_whitespace? + params[:w] == '1' + end + + def params_with_whitespace + hide_whitespace? ? request.query_parameters.except(:w) : request.query_parameters.merge(w: 1) + end + + def toggle_whitespace_link(url, options) + options[:class] ||= '' + options[:class] << ' btn btn-default' + + link_to "#{hide_whitespace? ? 'Show' : 'Hide'} whitespace changes", url, class: options[:class] + end end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index afe1e11a0da..198d39455d7 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -16,31 +16,49 @@ module IssuesHelper def url_for_project_issues(project = @project, options = {}) return '' if project.nil? - if options[:only_path] - project.issues_tracker.project_path - else - project.issues_tracker.project_url - end + url = + if options[:only_path] + project.issues_tracker.project_path + else + project.issues_tracker.project_url + end + + # Ensure we return a valid URL to prevent possible XSS. + URI.parse(url).to_s + rescue URI::InvalidURIError + '' end def url_for_new_issue(project = @project, options = {}) return '' if project.nil? - if options[:only_path] - project.issues_tracker.new_issue_path - else - project.issues_tracker.new_issue_url - end + url = + if options[:only_path] + project.issues_tracker.new_issue_path + else + project.issues_tracker.new_issue_url + end + + # Ensure we return a valid URL to prevent possible XSS. + URI.parse(url).to_s + rescue URI::InvalidURIError + '' end def url_for_issue(issue_iid, project = @project, options = {}) return '' if project.nil? - if options[:only_path] - project.issues_tracker.issue_path(issue_iid) - else - project.issues_tracker.issue_url(issue_iid) - end + url = + if options[:only_path] + project.issues_tracker.issue_path(issue_iid) + else + project.issues_tracker.issue_url(issue_iid) + end + + # Ensure we return a valid URL to prevent possible XSS. + URI.parse(url).to_s + rescue URI::InvalidURIError + '' end def bulk_update_milestone_options diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 3dded7c2f23..c99b137cdaa 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -37,7 +37,7 @@ module LabelsHelper link = send("namespace_project_#{type.to_s.pluralize}_path", project.namespace, project, - label_name: label.name) + label_name: [label.name]) if block_given? link_to link, &block diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 8a97a74ad73..24c4c098c65 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -19,6 +19,16 @@ module SearchHelper end end + def search_entries_info(collection, scope, term) + return unless collection.count > 0 + + from = collection.offset_value + 1 + to = collection.offset_value + collection.length + count = collection.total_count + + "Showing #{from} - #{to} of #{count} #{scope.humanize(capitalize: false)} for \"#{term}\"" + end + private # Autocomplete results for various settings pages diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index cdc40b81ee1..96116e916dd 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -28,6 +28,14 @@ module Emails mail_answer_thread(@merge_request, note_thread_options(recipient_id)) end + def note_snippet_email(recipient_id, note_id) + setup_note_mail(note_id, recipient_id) + + @snippet = @note.noteable + @target_url = namespace_project_snippet_url(*note_target_url_options) + mail_answer_thread(@snippet, note_thread_options(recipient_id)) + end + private def note_target_url_options diff --git a/app/models/blob.rb b/app/models/blob.rb index 72e6c5fa3fd..0fea6b7f576 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -19,6 +19,14 @@ class Blob < SimpleDelegator new(blob) end + def no_highlighting? + size && size > 1.megabyte + end + + def only_display_raw? + size && size > 5.megabytes + end + def svg? text? && language && language.name == 'SVG' end diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index 5b8e3f654ea..7bcc78247ba 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -8,7 +8,7 @@ module Milestoneish end def complete?(user = nil) - total_items_count(user) == closed_items_count(user) + total_items_count(user) > 0 && total_items_count(user) == closed_items_count(user) end def percent_complete(user = nil) diff --git a/app/models/concerns/statuseable.rb b/app/models/concerns/statuseable.rb index 8a293b7b76e..3ef91caad47 100644 --- a/app/models/concerns/statuseable.rb +++ b/app/models/concerns/statuseable.rb @@ -18,7 +18,7 @@ module Statuseable WHEN (#{builds})=0 THEN NULL WHEN (#{builds})=(#{success})+(#{ignored}) THEN 'success' WHEN (#{builds})=(#{pending}) THEN 'pending' - WHEN (#{builds})=(#{canceled}) THEN 'canceled' + WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored}) THEN 'canceled' WHEN (#{builds})=(#{skipped}) THEN 'skipped' WHEN (#{running})+(#{pending})>0 THEN 'running' ELSE 'failed' diff --git a/app/models/event.rb b/app/models/event.rb index 12183524b79..897518aadc7 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -345,7 +345,7 @@ class Event < ActiveRecord::Base end def reset_project_activity - if project + if project && Gitlab::ExclusiveLease.new("project:update_last_activity_at:#{project.id}", timeout: 60).try_obtain project.update_column(:last_activity_at, self.created_at) end end diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index bc6e0f98c3c..d149511b868 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -16,6 +16,7 @@ # note_events :boolean default(FALSE), not null # enable_ssl_verification :boolean default(TRUE) # build_events :boolean default(FALSE), not null +# token :string # class ProjectHook < WebHook diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb index 80962264ba2..f45145eeb3a 100644 --- a/app/models/hooks/service_hook.rb +++ b/app/models/hooks/service_hook.rb @@ -16,6 +16,7 @@ # note_events :boolean default(FALSE), not null # enable_ssl_verification :boolean default(TRUE) # build_events :boolean default(FALSE), not null +# token :string # class ServiceHook < WebHook diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb index 15dddcc2447..012cc8ec005 100644 --- a/app/models/hooks/system_hook.rb +++ b/app/models/hooks/system_hook.rb @@ -16,6 +16,7 @@ # note_events :boolean default(FALSE), not null # enable_ssl_verification :boolean default(TRUE) # build_events :boolean default(FALSE), not null +# token :string # class SystemHook < WebHook diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 3a2e4f546f7..1e3b4815596 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -16,6 +16,7 @@ # note_events :boolean default(FALSE), not null # enable_ssl_verification :boolean default(TRUE) # build_events :boolean default(FALSE), not null +# token :string # class WebHook < ActiveRecord::Base @@ -43,23 +44,17 @@ class WebHook < ActiveRecord::Base if parsed_url.userinfo.blank? response = WebHook.post(url, body: data.to_json, - headers: { - "Content-Type" => "application/json", - "X-Gitlab-Event" => hook_name.singularize.titleize - }, + headers: build_headers(hook_name), verify: enable_ssl_verification) else - post_url = url.gsub("#{parsed_url.userinfo}@", "") + post_url = url.gsub("#{parsed_url.userinfo}@", '') auth = { username: CGI.unescape(parsed_url.user), password: CGI.unescape(parsed_url.password), } response = WebHook.post(post_url, body: data.to_json, - headers: { - "Content-Type" => "application/json", - "X-Gitlab-Event" => hook_name.singularize.titleize - }, + headers: build_headers(hook_name), verify: enable_ssl_verification, basic_auth: auth) end @@ -73,4 +68,15 @@ class WebHook < ActiveRecord::Base def async_execute(data, hook_name) Sidekiq::Client.enqueue(ProjectWebHookWorker, id, data, hook_name) end + + private + + def build_headers(hook_name) + headers = { + 'Content-Type' => 'application/json', + 'X-Gitlab-Event' => hook_name.singularize.titleize + } + headers['X-Gitlab-Token'] = token if token.present? + headers + end end diff --git a/app/models/project.rb b/app/models/project.rb index 70921e02759..3e782be637e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -745,19 +745,17 @@ class Project < ActiveRecord::Base end def open_branches - all_branches = repository.branches + # We're using a Set here as checking values in a large Set is faster than + # checking values in a large Array. + protected_set = Set.new(protected_branch_names) - if protected_branches.present? - all_branches.reject! do |branch| - protected_branches_names.include?(branch.name) - end + repository.branches.reject do |branch| + protected_set.include?(branch.name) end - - all_branches end - def protected_branches_names - @protected_branches_names ||= protected_branches.map(&:name) + def protected_branch_names + @protected_branch_names ||= protected_branches.pluck(:name) end def root_ref?(branch) @@ -774,7 +772,7 @@ class Project < ActiveRecord::Base # Check if current branch name is marked as protected in the system def protected_branch?(branch_name) - protected_branches_names.include?(branch_name) + protected_branches.where(name: branch_name).any? end def developers_can_push_to_protected_branch?(branch_name) @@ -911,6 +909,7 @@ class Project < ActiveRecord::Base repository.rugged.references.create('HEAD', "refs/heads/#{branch}", force: true) + repository.copy_gitattributes(branch) reload_default_branch end diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index 3efbfd2eec3..861cc974ec4 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -26,7 +26,7 @@ class BuildkiteService < CiService prop_accessor :project_url, :token, :enable_ssl_verification - validates :project_url, presence: true, if: :activated? + validates :project_url, presence: true, url: true, if: :activated? validates :token, presence: true, if: :activated? after_save :compose_service_hook, if: :activated? @@ -91,7 +91,7 @@ class BuildkiteService < CiService { type: 'text', name: 'project_url', placeholder: "#{ENDPOINT}/example/project" }, - + { type: 'checkbox', name: 'enable_ssl_verification', title: "Enable SSL verification" } diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 25045224ce5..c5501e06411 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -21,7 +21,7 @@ class IssueTrackerService < Service - validates :project_url, :issues_url, :new_issue_url, presence: true, if: :activated? + validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated? default_value_for :category, 'issue_tracker' diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 1ed42c4f3e7..b4418ba9284 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -28,6 +28,8 @@ class JiraService < IssueTrackerService prop_accessor :username, :password, :api_url, :jira_issue_transition_id, :title, :description, :project_url, :issues_url, :new_issue_url + validates :api_url, presence: true, url: true, if: :activated? + before_validation :set_api_url, :set_jira_issue_transition_id before_update :reset_password diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index fd65027f084..7092b757549 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -22,7 +22,7 @@ class SlackService < Service prop_accessor :webhook, :username, :channel boolean_accessor :notify_only_broken_builds - validates :webhook, presence: true, if: :activated? + validates :webhook, presence: true, url: true, if: :activated? def initialize_properties if properties.nil? diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb index 1f7d85a5f3d..d48f0546159 100644 --- a/app/models/project_snippet.rb +++ b/app/models/project_snippet.rb @@ -22,4 +22,6 @@ class ProjectSnippet < Snippet # Scopes scope :fresh, -> { order("created_at DESC") } + + participant :author, :notes end diff --git a/app/models/repository.rb b/app/models/repository.rb index 61c8dce6060..b4319297e43 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -457,7 +457,7 @@ class Repository def changelog cache.fetch(:changelog) do tree(:head).blobs.find do |file| - file.name =~ /\A(changelog|history)/i + file.name =~ /\A(changelog|history|changes|news)/i end end end @@ -938,6 +938,16 @@ class Repository raw_repository.ls_files(actual_ref) end + def copy_gitattributes(ref) + actual_ref = ref || root_ref + begin + raw_repository.copy_gitattributes(actual_ref) + true + rescue Gitlab::Git::Repository::InvalidRef + false + end + end + def main_language return if empty? || rugged.head_unborn? diff --git a/app/models/snippet.rb b/app/models/snippet.rb index b96e3937281..0fd08061925 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -112,6 +112,10 @@ class Snippet < ActiveRecord::Base visibility_level end + def no_highlighting? + content.lines.count > 1000 + end + class << self # Searches for snippets with a matching title or file name. # diff --git a/app/models/user.rb b/app/models/user.rb index ab48f8f1960..b6f405c6981 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -91,7 +91,7 @@ class User < ActiveRecord::Base devise :two_factor_backupable, otp_number_of_backup_codes: 10 serialize :otp_backup_codes, JSON - devise :lockable, :async, :recoverable, :rememberable, :trackable, + devise :lockable, :recoverable, :rememberable, :trackable, :validatable, :omniauthable, :confirmable, :registerable attr_accessor :force_random_password diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 1e1be8cd04b..b7af80055bf 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -42,7 +42,12 @@ class GitPushService < BaseService # Collect data for this git push @push_commits = @project.repository.commits_between(params[:oldrev], params[:newrev]) process_commit_messages + + # Update the bare repositories info/attributes file using the contents of the default branches + # .gitattributes file + update_gitattributes if is_default_branch? end + # Update merge requests that may be affected by this push. A new branch # could cause the last commit of a merge request to change. update_merge_requests @@ -54,6 +59,10 @@ class GitPushService < BaseService perform_housekeeping end + def update_gitattributes + @project.repository.copy_gitattributes(params[:ref]) + end + def update_main_language # Performance can be bad so for now only check main_language once # See https://gitlab.com/gitlab-org/gitlab-ce/issues/14937 diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index fa34753c4fd..3544752d47a 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -7,6 +7,9 @@ module MergeRequests merge_request.can_be_created = false merge_request.compare_commits = [] merge_request.source_project = project unless merge_request.source_project + + merge_request.target_project = nil unless can?(current_user, :read_project, merge_request.target_project) + merge_request.target_project ||= (project.forked_from_project || project) merge_request.target_branch ||= merge_request.target_project.default_branch diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index 2bb312bb252..01586994813 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -5,6 +5,8 @@ module Notes note.author = current_user note.system = false + return unless valid_project?(note) + if note.save # Finish the harder work in the background NewNoteWorker.perform_in(2.seconds, note.id, params) @@ -13,5 +15,14 @@ module Notes note end + + private + + def valid_project?(note) + return false unless project + return true if note.for_commit? + + note.noteable.try(:project) == project + end end end diff --git a/app/services/wiki_pages/create_service.rb b/app/services/wiki_pages/create_service.rb index 988c663b9d0..24a817c06c9 100644 --- a/app/services/wiki_pages/create_service.rb +++ b/app/services/wiki_pages/create_service.rb @@ -1,7 +1,8 @@ module WikiPages class CreateService < WikiPages::BaseService def execute - page = WikiPage.new(@project.wiki) + project_wiki = ProjectWiki.new(@project, current_user) + page = WikiPage.new(project_wiki) if page.create(@params) execute_hooks(page, 'create') diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index 67d23c80233..7b388cf7862 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -13,9 +13,15 @@ = form_errors(@hook) .form-group - = f.label :url, "URL:", class: 'control-label' + = f.label :url, 'URL', class: 'control-label' .col-sm-10 - = f.text_field :url, class: "form-control" + = f.text_field :url, class: 'form-control' + .form-group + = f.label :token, 'Secret Token', class: 'control-label' + .col-sm-10 + = f.text_field :token, class: 'form-control' + %p.help-block + Use this token to validate received payloads .form-group = f.label :url, "Trigger", class: 'control-label' .col-sm-10.prepend-top-10 diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml index 79df17ba612..3998e66f40d 100644 --- a/app/views/doorkeeper/applications/index.html.haml +++ b/app/views/doorkeeper/applications/index.html.haml @@ -44,7 +44,7 @@ = icon('pencil') = render 'delete_form', application: application, small: true - else - .profile-settings-message.text-center + .settings-message.text-center You don't have any applications .oauth-authorized-applications.prepend-top-20.append-bottom-default - if user_oauth_applications? @@ -78,5 +78,5 @@ %td= token.scopes %td= render 'doorkeeper/authorized_applications/delete_form', token: token - else - .profile-settings-message.text-center + .settings-message.text-center You don't have any authorized applications diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 4d20dd5830e..5d622582088 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -4,7 +4,12 @@ #{time_ago_with_tooltip(event.created_at)} = cache [event, current_application_settings, "v2.2"] do - = image_tag avatar_icon(event.author_email, 40), class: "avatar s40", alt:'' + - if event.author + = link_to user_path(event.author.username) do + = image_tag avatar_icon(event.author_email, 40), class: "avatar s40", alt:'' + - else + = image_tag avatar_icon(event.author_email, 40), class: "avatar s40", alt:'' + - if event.created_project? = render "events/event/created_project", event: event - elsif event.push? diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index da3c3711cdd..70e88da7aae 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -21,7 +21,7 @@ %tr %td.shortcut .key ? - %td Show this dialog + %td Show/hide this dialog %tr %td.shortcut - if browser.mac? @@ -169,6 +169,10 @@ %td.shortcut .key t %td Go to finding file + %tr + %td.shortcut + .key i + %td New issue .col-lg-4 %table.shortcut-mappings %tbody{ class: 'hidden-shortcut network', style: 'display:none' } @@ -241,6 +245,10 @@ %td.shortcut .key e %td Edit issue + %tr + %td.shortcut + .key l + %td Change Label %tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' } %tr %th @@ -261,3 +269,7 @@ %td.shortcut .key e %td Edit merge request + %tr + %td.shortcut + .key l + %td Change Label diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index ca9c2a0bf2e..ad8a2e1e6c7 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -22,13 +22,13 @@ = image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36' .username = current_user.username + - if defined?(nav) && nav + .layout-nav + .container-fluid + = render "layouts/nav/#{nav}" .content-wrapper = render "layouts/flash" = yield :flash_message - - if defined?(nav) && nav - .layout-nav - %div{ class: container_class } - = render "layouts/nav/#{nav}" %div{ class: (container_class unless @no_container) } .content .clearfix diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 3beb8ff7c0d..cde9e1b918b 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -15,7 +15,7 @@ - if current_user - if session[:impersonator_id] %li.impersonation - = link_to stop_impersonation_admin_users_path, method: :delete, title: 'Stop Impersonation', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do + = link_to admin_impersonation_path, method: :delete, title: 'Stop Impersonation', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do = icon('user-secret fw') - if current_user.is_admin? %li diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index a15b7758c4b..479bde33719 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -124,3 +124,8 @@ %li.hidden = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do Network + + -# Shortcut to create a new issue + %li.hidden + = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'shortcuts-new-issue' do + Create a new issue diff --git a/app/views/notify/note_snippet_email.html.haml b/app/views/notify/note_snippet_email.html.haml new file mode 100644 index 00000000000..2fa2f784661 --- /dev/null +++ b/app/views/notify/note_snippet_email.html.haml @@ -0,0 +1 @@ += render 'note_message' diff --git a/app/views/notify/note_snippet_email.text.erb b/app/views/notify/note_snippet_email.text.erb new file mode 100644 index 00000000000..4d5a406f4b0 --- /dev/null +++ b/app/views/notify/note_snippet_email.text.erb @@ -0,0 +1,8 @@ +New comment for Snippet <%= @snippet.id %> + +<%= url_for(namespace_project_snippet_url(@snippet.project.namespace, @snippet.project, @snippet, anchor: "note_#{@note.id}")) %> + + +Author: <%= @note.author_name %> + +<%= @note.note %> diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index 57527361eb6..6f7fefdb46d 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -45,4 +45,4 @@ %span.label.label-info Public Email - if email.email === current_user.notification_email %span.label.label-info Notification Email - = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove pull-right' + = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-warning prepend-left-10' diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml index 4dbaa662b66..3276db6692c 100644 --- a/app/views/profiles/keys/_key.html.haml +++ b/app/views/profiles/keys/_key.html.haml @@ -1,6 +1,6 @@ %li.key-list-item .pull-left.append-right-10 - = icon 'key', class: "key-icon hidden-xs" + = icon 'key', class: "settings-list-icon hidden-xs" .key-list-item-info = link_to path_to_key(key, is_admin), class: "title" do = key.title diff --git a/app/views/profiles/keys/_key_table.html.haml b/app/views/profiles/keys/_key_table.html.haml index 296cafa6e31..e78763bdcb2 100644 --- a/app/views/profiles/keys/_key_table.html.haml +++ b/app/views/profiles/keys/_key_table.html.haml @@ -4,7 +4,7 @@ %ul.well-list = render partial: 'profiles/keys/key', collection: @keys, locals: { is_admin: is_admin } - else - %p.profile-settings-message.text-center + %p.settings-message.text-center - if is_admin There are no SSH keys associated with this account. - else diff --git a/app/views/projects/blob/_text.html.haml b/app/views/projects/blob/_text.html.haml index d09cd73558c..b1769759dce 100644 --- a/app/views/projects/blob/_text.html.haml +++ b/app/views/projects/blob/_text.html.haml @@ -1,10 +1,19 @@ -- blob.load_all_data!(@repository) -- if markup?(blob.name) - .file-content.wiki - = render_markup(blob.name, blob.data) +- if blob.only_display_raw? + .file-content.code + .nothing-here-block + File too large, you can + = succeed '.' do + = link_to 'view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank' + - else - - unless blob.empty? - = render 'shared/file_highlight', blob: blob + - blob.load_all_data!(@repository) + + - if markup?(blob.name) + .file-content.wiki + = render_markup(blob.name, blob.data) - else - .file-content.code - .nothing-here-block Empty file + - if blob.empty? + .file-content.code + .nothing-here-block Empty file + - else + = render 'shared/file_highlight', blob: blob diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml index 25714e6cb47..d3acd33116c 100644 --- a/app/views/projects/commit/_ci_commit.html.haml +++ b/app/views/projects/commit/_ci_commit.html.haml @@ -16,7 +16,7 @@ - if defined?(link_to_commit) && link_to_commit for commit = link_to ci_commit.short_sha, namespace_project_commit_path(@project.namespace, @project, ci_commit.sha), class: "monospace" - - if ci_commit.duration > 0 + - if ci_commit.duration in = time_interval_in_words ci_commit.duration diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml index 8d66bae8cdf..450aaeb367c 100644 --- a/app/views/projects/deploy_keys/_deploy_key.html.haml +++ b/app/views/projects/deploy_keys/_deploy_key.html.haml @@ -1,32 +1,27 @@ %li - .pull-right + .pull-left.append-right-10.hidden-xs + = icon "key", class: "key-icon" + .deploy-key-content.key-list-item-info + %strong.title + = deploy_key.title + .description + = deploy_key.fingerprint + .deploy-key-content.prepend-left-default.deploy-key-projects + - deploy_key.projects.each do |project| + - if can?(current_user, :read_project, project) + = link_to namespace_project_path(project.namespace, project), class: "label deploy-project-label" do + = project.name_with_namespace + .deploy-key-content + %span.key-created-at + created #{time_ago_with_tooltip(deploy_key.created_at)} + .visible-xs-block.visible-sm-block - if @available_keys.include?(deploy_key) - = link_to enable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: 'btn btn-sm', method: :put do - = icon('plus') + = link_to enable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: "btn btn-sm prepend-left-10", method: :put do Enable - else - if deploy_key.destroyed_when_orphaned? && deploy_key.almost_orphaned? - = link_to 'Remove', disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), data: { confirm: 'You are going to remove deploy key. Are you sure?'}, method: :put, class: "btn btn-remove delete-key btn-sm pull-right" + = link_to disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), data: { confirm: "You are going to remove deploy key. Are you sure?" }, method: :put, class: "btn btn-warning btn-sm prepend-left-10" do + Remove - else - = link_to disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: 'btn btn-sm', method: :put do - = icon('power-off') + = link_to disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: "btn btn-warning btn-sm prepend-left-10", method: :put do Disable - - = icon('key') - %strong= deploy_key.title - %br - %code.key-fingerprint= deploy_key.fingerprint - - %p.light.prepend-top-10 - - if deploy_key.public? - %span.label.label-info.deploy-project-label - Public deploy key - - - deploy_key.projects.each do |project| - - if can?(current_user, :read_project, project) - %span.label.label-gray.deploy-project-label - = link_to namespace_project_path(project.namespace, project) do - = project.name_with_namespace - - %small.pull-right - Created #{time_ago_with_tooltip(deploy_key.created_at)} diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml index f6565f85836..894c36a96df 100644 --- a/app/views/projects/deploy_keys/_form.html.haml +++ b/app/views/projects/deploy_keys/_form.html.haml @@ -1,18 +1,13 @@ -%div - = form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: 'deploy-key-form form-horizontal js-requires-input' } do |f| - = form_errors(@key) - - .form-group - = f.label :title, class: "control-label" - .col-sm-10= f.text_field :title, class: 'form-control', autofocus: true, required: true - .form-group - = f.label :key, class: "control-label" - .col-sm-10 - %p.light - Paste a machine public key here. Read more about how to generate it - = link_to "here", help_page_path("ssh", "README") - = f.text_area :key, class: "form-control thin_area", rows: 5, required: true - - .form-actions - = f.submit 'Create Deploy Key', class: "btn-create btn" - = link_to "Cancel", namespace_project_deploy_keys_path(@project.namespace, @project), class: "btn btn-cancel" += form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input" } do |f| + = form_errors(@key) + .form-group + = f.label :title, class: "label-light" + = f.text_field :title, class: 'form-control', autofocus: true, required: true + .form-group + = f.label :key, class: "label-light" + = f.text_area :key, class: "form-control", rows: 5, required: true + .form-group + %p.light.append-bottom-0 + Paste a machine public key here. Read more about how to generate it + = link_to "here", help_page_path("ssh", "README") + = f.submit "Add key", class: "btn-create btn" diff --git a/app/views/projects/deploy_keys/index.html.haml b/app/views/projects/deploy_keys/index.html.haml index 8e24c778b7c..e230834e8ba 100644 --- a/app/views/projects/deploy_keys/index.html.haml +++ b/app/views/projects/deploy_keys/index.html.haml @@ -1,43 +1,36 @@ - page_title "Deploy Keys" -%h3.page-title - Deploy keys allow read-only access to the repository - - = link_to new_namespace_project_deploy_key_path(@project.namespace, @project), class: "btn btn-new pull-right", title: "New Deploy Key" do - %i.fa.fa-plus - New Deploy Key - -%p.light - Deploy keys can be used for CI, staging or production servers. - You can create a deploy key or add an existing one - -%hr.clearfix - -.row - .col-md-6.enabled-keys - %h5 - %strong.cgreen Enabled deploy keys - for this project - %ul.bordered-list - = render @enabled_keys - - if @enabled_keys.blank? - .light-well - .nothing-here-block Create a #{link_to 'new deploy key', new_namespace_project_deploy_key_path(@project.namespace, @project)} or add an existing one - .col-md-6.available-keys - - # If there are available public deploy keys but no available project deploy keys, only public deploy keys are shown. - - if @available_project_keys.any? || @available_public_keys.blank? - %h5 - %strong Deploy keys - from projects you have access to - %ul.bordered-list +.row.prepend-top-default + .col-lg-3.profile-settings-sidebar + %h4.prepend-top-0 + = page_title + %p + Deploy keys allow read-only access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one. + .col-lg-9 + %h5.prepend-top-0 + Create a new deploy key for this project + = render "form" + .col-lg-9.col-lg-offset-3 + %hr + .col-lg-9.col-lg-offset-3.append-bottom-default.deploy-keys + %h5.prepend-top-0 + Enabled deploy keys for this project (#{@enabled_keys.size}) + - if @enabled_keys.any? + %ul.well-list + = render @enabled_keys + - else + .profile-settings-message.text-center + No deploy keys found. Create one with the form above or add existing one below. + %h5.prepend-top-default + Deploy keys from projects you have access to (#{@available_project_keys.size}) + - if @available_project_keys.any? + %ul.well-list = render @available_project_keys - - if @available_project_keys.blank? - .light-well - .nothing-here-block Deploy keys from projects you have access to will be displayed here - + - else + .profile-settings-message.text-center + No deploy keys from your projects could be found. Create one with the form above or add existing one below. - if @available_public_keys.any? - %h5 - %strong Public deploy keys - available to any project - %ul.bordered-list + %h5.prepend-top-default + Public deploy keys available to any project (#{@available_public_keys.size}) + %ul.well-list = render @available_public_keys diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index eaab99973a4..d9c4b410d32 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -1,3 +1,4 @@ +- show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true) - if diff_view == 'parallel' - fluid_layout true @@ -5,6 +6,11 @@ .content-block.oneline-block.files-changed .inline-parallel-buttons + - if show_whitespace_toggle + - if current_controller?(:commit) + = commit_diff_whitespace_link(@project, @commit, class: 'hidden-xs') + - elsif current_controller?(:merge_requests) + = diff_merge_request_whitespace_link(@project, @merge_request, class: 'hidden-xs') .btn-group = inline_diff_btn = parallel_diff_btn diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 83a8d7ae9bf..0f04fc5d33c 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -40,19 +40,19 @@ = view_file_btn(diff_commit.id, diff_file, project) .diff-content.diff-wrap-lines - -# Skipp all non non-supported blobs + - # Skip all non non-supported blobs - return unless blob.respond_to?('text?') - if diff_file.too_large? - .nothing-here-block - This diff could not be displayed because it is too large. - - else - - 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 + .nothing-here-block This diff could not be displayed because it is too large. + - elsif blob_text_viewable?(blob) && !project.repository.diffable?(blob) + .nothing-here-block This diff was suppressed by a .gitattributes entry. + - elsif blob_text_viewable?(blob) + - if diff_view == 'parallel' + = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i - else - .nothing-here-block No preview for this file type + = 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/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml index 13f5fc141fa..2b904544f28 100644 --- a/app/views/projects/group_links/index.html.haml +++ b/app/views/projects/group_links/index.html.haml @@ -1,41 +1,44 @@ - page_title "Groups" -%h3.page_title Share project with other groups -%p.light - Projects can be stored in only one group at once. However you can share a project with other groups here. -%hr -- if @group_links.present? - .enabled-groups.panel.panel-default - .panel-heading - Already shared with - %ul.well-list - - @group_links.each do |group_link| - - group = group_link.group - %li - .pull-right - = link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: 'btn btn-sm' do - %i.icon-remove - disable sharing - = link_to group do - %strong - %i.icon-folder-open - = group.name - %br - .light up to #{group_link.human_access} - - -.available-groups - %h4 - Can be shared with - %div - = form_tag namespace_project_group_links_path(@project.namespace, @project), method: :post, class: 'form-horizontal' do +.row.prepend-top-default + .col-lg-3.settings-sidebar + %h4.prepend-top-0 + Share project with other groups + %p + Projects can be stored in only one group at once. However you can share a project with other groups here. + .col-lg-9 + %h5.prepend-top-0 + Set a group to share + = form_tag namespace_project_group_links_path(@project.namespace, @project), method: :post do .form-group - = label_tag :link_group_id, 'Group', class: 'control-label' - .col-sm-10 - = groups_select_tag(:link_group_id, skip_group: @project.group.try(:path)) + = label_tag :link_group_id, "Group", class: "label-light" + = groups_select_tag(:link_group_id, skip_group: @project.group.try(:path)) .form-group - = label_tag :link_group_access, 'Max access level', class: 'control-label' - .col-sm-10 - = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control" - .form-actions - = submit_tag "Share", class: "btn btn-create" - + = label_tag :link_group_access, "Max access level", class: "label-light" + .select-wrapper + = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control" + %span.caret + = submit_tag "Share", class: "btn btn-create" + .col-lg-9.col-lg-offset-3 + %hr + .col-lg-9.col-lg-offset-3.append-bottom-default.enabled-groups + %h5.prepend-top-0 + Groups you share with (#{@group_links.size}) + - if @group_links.present? + %ul.well-list + - @group_links.each do |group_link| + - group = group_link.group + %li + .pull-left.append-right-10.hidden-xs + = icon("folder-open-o", class: "settings-list-icon") + .pull-left + = link_to group do + = group.name + %br + up to #{group_link.human_access} + .pull-right + = link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: "btn btn-transparent" do + %span.sr-only disable sharing + = icon("trash") + - else + .settings-message.text-center + There are no groups with access to your project, add one in the form above diff --git a/app/views/projects/hooks/_project_hook.html.haml b/app/views/projects/hooks/_project_hook.html.haml new file mode 100644 index 00000000000..62eba5888a4 --- /dev/null +++ b/app/views/projects/hooks/_project_hook.html.haml @@ -0,0 +1,15 @@ +%li + .row + .col-md-8.col-lg-7 + %strong.light-header= hook.url + %div + - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger| + - if hook.send(trigger) + %span.label.label-gray.deploy-project-label= trigger.titleize + .col-md-4.col-lg-5.text-right-lg.prepend-top-5 + %span.append-right-10.inline + SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} + = link_to "Test", test_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm" + = link_to namespace_project_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-transparent" do + %span.sr-only Remove + = icon('trash') diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index aae3abcad4b..36c1d69f060 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -1,88 +1,84 @@ - page_title "Webhooks" -%h3.page-title - Webhooks +.row.prepend-top-default + .col-lg-3.profile-settings-sidebar + %h4.prepend-top-0 + = page_title + %p + #{link_to "Webhooks", help_page_path("web_hooks", "web_hooks")} can be + used for binding events when something is happening within the project. + .col-lg-9.append-bottom-default + %h5.prepend-top-0 + Add new webhook + = form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: namespace_project_hooks_path(@project.namespace, @project) do |f| + = form_errors(@hook) -%p.light - #{link_to "Webhooks ", help_page_path("web_hooks", "web_hooks"), class: "vlink"} can be - used for binding events when something is happening within the project. - -%hr.clearfix - -= form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: namespace_project_hooks_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f| - = form_errors(@hook) - - .form-group - = f.label :url, "URL", class: 'control-label' - .col-sm-10 - = f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json' - .form-group - = f.label :url, "Trigger", class: 'control-label' - .col-sm-10.prepend-top-10 - %div - = f.check_box :push_events, class: 'pull-left' - .prepend-left-20 - = f.label :push_events, class: 'list-label' do - %strong Push events - %p.light - This url will be triggered by a push to the repository - %div - = f.check_box :tag_push_events, class: 'pull-left' - .prepend-left-20 - = f.label :tag_push_events, class: 'list-label' do - %strong Tag push events - %p.light - This url will be triggered when a new tag is pushed to the repository - %div - = f.check_box :note_events, class: 'pull-left' - .prepend-left-20 - = f.label :note_events, class: 'list-label' do - %strong Comments - %p.light - This url will be triggered when someone adds a comment - %div - = f.check_box :issues_events, class: 'pull-left' - .prepend-left-20 - = f.label :issues_events, class: 'list-label' do - %strong Issues events - %p.light - This url will be triggered when an issue is created/updated/merged - %div - = f.check_box :merge_requests_events, class: 'pull-left' - .prepend-left-20 - = f.label :merge_requests_events, class: 'list-label' do - %strong Merge Request events - %p.light - This url will be triggered when a merge request is created/updated/merged - %div - = f.check_box :build_events, class: 'pull-left' - .prepend-left-20 - = f.label :build_events, class: 'list-label' do - %strong Build events - %p.light - This url will be triggered when the build status changes - .form-group - = f.label :enable_ssl_verification, "SSL verification", class: 'control-label checkbox' - .col-sm-10 - .checkbox - = f.label :enable_ssl_verification do - = f.check_box :enable_ssl_verification - %strong Enable SSL verification - .form-actions - = f.submit "Add Webhook", class: "btn btn-create" - --if @hooks.any? - .panel.panel-default - .panel-heading + .form-group + = f.label :url, "URL", class: "label-light" + = f.text_field :url, class: "form-control", placeholder: "http://example.com/trigger-ci.json" + .form-group + = f.label :token, "Secret Token", class: 'label-light' + = f.text_field :token, class: "form-control", placeholder: '' + %p.help-block + Use this token to validate received payloads + .form-group + = f.label :url, "Trigger", class: "label-light" + %div + = f.check_box :push_events, class: "pull-left" + .prepend-left-20 + = f.label :push_events, class: "label-light append-bottom-0" do + Push events + %p.light + This url will be triggered by a push to the repository + %div + = f.check_box :tag_push_events, class: "pull-left" + .prepend-left-20 + = f.label :tag_push_events, class: "label-light append-bottom-0" do + Tag push events + %p.light + This url will be triggered when a new tag is pushed to the repository + %div + = f.check_box :note_events, class: "pull-left" + .prepend-left-20 + = f.label :note_events, class: "label-light append-bottom-0" do + Comments + %p.light + This url will be triggered when someone adds a comment + %div + = f.check_box :issues_events, class: "pull-left" + .prepend-left-20 + = f.label :issues_events, class: "label-light append-bottom-0" do + Issues events + %p.light + This url will be triggered when an issue is created/updated/merged + %div + = f.check_box :merge_requests_events, class: "pull-left" + .prepend-left-20 + = f.label :merge_requests_events, class: "label-light append-bottom-0" do + Merge Request events + %p.light + This url will be triggered when a merge request is created/updated/merged + %div + = f.check_box :build_events, class: "pull-left" + .prepend-left-20 + = f.label :build_events, class: "label-light append-bottom-0" do + Build events + %p.light + This url will be triggered when the build status changes + .form-group + = f.label :enable_ssl_verification, "SSL verification", class: "label-light" + %div + = f.check_box :enable_ssl_verification, class: "pull-left" + .prepend-left-20 + = f.label :enable_ssl_verification, class: "label-light append-bottom-0" do + Enable SSL verification + = f.submit "Add Webhook", class: "btn btn-create" + %hr + %h5.prepend-top-default Webhooks (#{@hooks.count}) - %ul.content-list - - @hooks.each do |hook| - %li - .controls - = link_to 'Test Hook', test_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm btn-grouped" - = link_to 'Remove', namespace_project_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" - .monospace= hook.url - %div - - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger| - - if hook.send(trigger) - %span.label.label-gray= trigger.titleize - %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} + - if @hooks.any? + %ul.well-list + - @hooks.each do |hook| + = render "project_hook", hook: hook + - else + %p.profile-settings-message.text-center.append-bottom-0 + No webhooks found, add one in the form above. diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml index 6da8e4f33a9..469429ccf3c 100644 --- a/app/views/projects/issues/_new_branch.html.haml +++ b/app/views/projects/issues/_new_branch.html.haml @@ -1,5 +1,13 @@ -- if current_user && can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user) +- if can?(current_user, :push_code, @project) .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 has-tooltip', title: @issue.to_branch_name do - = icon('code-fork') - New Branch + #new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)} + = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), method: :post, class: 'btn has-tooltip', title: @issue.to_branch_name, disabled: 'disabled' do + .checking + %i.fa.fa-spinner.fa-spin + Checking branches + .available(style="display: none") + %i.fa.fa-code-fork + New branch + .unavailable(style="display: none") + %i.fa.fa-exclamation-triangle + New branch unavailable diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 2f14a91e64f..18b3f9e1549 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -42,7 +42,7 @@ %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits. %p To preserve performance the line changes are not shown. - else - = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @merge_request.diff_refs + = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @merge_request.diff_refs, show_whitespace_toggle: false - if @ci_commit #builds.builds.tab-pane = render "projects/merge_requests/show/builds" diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index be63875ab34..56543ccd062 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -42,9 +42,12 @@ = preserve do = markdown @milestone.description -- if @milestone.complete?(current_user) && @milestone.active? +- if @milestone.total_items_count(current_user).zero? .alert.alert-success.prepend-top-default - %span All issues for this milestone are closed. You may close milestone now. + %span Assign some issues to this milestone. +- elsif @milestone.complete?(current_user) && @milestone.active? + .alert.alert-success.prepend-top-default + %span All issues for this milestone are closed. You may close this milestone now. = render 'shared/milestones/summary', milestone: @milestone, project: @project = render 'shared/milestones/tabs', milestone: @milestone diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml index f68449b1863..b9e9dd8aaea 100644 --- a/app/views/projects/protected_branches/_branches_list.html.haml +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -1,35 +1,41 @@ -- unless @branches.empty? - %br - %h4 Already Protected: - .table-holder +%h5.prepend-top-0 + Already Protected (#{@branches.size}) +- if @branches.empty? + %p.profile-settings-message.text-center + No branches are protected, protect a branch with the form above. +- else + - can_admin_project = can?(current_user, :admin_project, @project) + .table-responsive %table.table.protected-branches-list + %colgroup + %col{ width: "30%" } + %col{ width: "30%" } + %col{ width: "25%" } + - if can_admin_project + %col %thead - %tr.no-border + %tr %th Branch - %th Developers can push %th Last commit - %th - + %th Developers can push + - if can_admin_project + %th %tbody - @branches.each do |branch| - @url = namespace_project_protected_branch_path(@project.namespace, @project, branch) %tr %td - = link_to namespace_project_commits_path(@project.namespace, @project, branch.name) do - %strong= branch.name - - if @project.root_ref?(branch.name) - %span.label.label-info default - %td - = check_box_tag "developers_can_push", branch.id, branch.developers_can_push, "data-url" => @url - %td - - if commit = branch.commit - = link_to namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id' do - = commit.short_id - · - #{time_ago_with_tooltip(commit.committed_date)} - - else - (branch was removed from repository) + = link_to(branch.name, namespace_project_commits_path(@project.namespace, @project, branch.name)) + - if @project.root_ref?(branch.name) + %span.label.label-info.prepend-left-5 default + %td + - if commit = branch.commit + = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id') + #{time_ago_with_tooltip(commit.committed_date)} + - else + (branch was removed from repository) + %td + = check_box_tag("developers_can_push", branch.id, branch.developers_can_push, data: { url: @url }) + - if can_admin_project %td - .pull-right - - if can? current_user, :admin_project, @project - = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm" + = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-warning btn-sm" diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 653b02da4db..c7d317dbaee 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -1,31 +1,33 @@ - page_title "Protected branches" -%h3.page-title Protected branches -%p.light Keep stable branches secure and force developers to use Merge Requests -%hr -.well - %p Protected branches are designed to - %ul - %li prevent pushes from everybody except #{link_to "masters", help_page_path("permissions", "permissions"), class: "vlink"} - %li prevent anyone from force pushing to the branch - %li prevent anyone from deleting the branch - %p Read more about #{link_to "project permissions", help_page_path("permissions", "permissions"), class: "underlined-link"} +.row.prepend-top-default.append-bottom-default + .col-lg-3 + %h4.prepend-top-0 + = page_title + %p Keep stable branches secure and force developers to use Merge Requests + .col-lg-9 + %h5.prepend-top-0 + Protect a branch + .account-well.append-bottom-default + %p.light-header.append-bottom-0 Protected branches are designed to + %ul + %li prevent pushes from everybody except #{link_to "masters", help_page_path("permissions", "permissions"), class: "vlink"} + %li prevent anyone from force pushing to the branch + %li prevent anyone from deleting the branch + %p.append-bottom-0 Read more about #{link_to "project permissions", help_page_path("permissions", "permissions"), class: "underlined-link"} + - if can? current_user, :admin_project, @project + = form_for [@project.namespace.becomes(Namespace), @project, @protected_branch] do |f| + = form_errors(@protected_branch) -- if can? current_user, :admin_project, @project - = form_for [@project.namespace.becomes(Namespace), @project, @protected_branch], html: { class: 'form-horizontal' } do |f| - = form_errors(@protected_branch) - - .form-group - = f.label :name, "Branch", class: 'control-label' - .col-sm-10 - = f.select(:name, @project.open_branches.map { |br| [br.name, br.name] } , {include_blank: true}, {class: "select2", data: {placeholder: "Select branch"}}) - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :developers_can_push do - = f.check_box :developers_can_push - %strong Developers can push - .help-block Allow developers to push to this branch - .form-actions - = f.submit 'Protect', class: "btn-create btn" -= render 'branches_list' + .form-group + = f.label :name, "Branch", class: "label-light" + = f.select(:name, @project.open_branches.map { |br| [br.name, br.name] } , {include_blank: true}, {class: "select2", data: {placeholder: "Select branch"}}) + .form-group + = f.check_box :developers_can_push, class: "pull-left" + .prepend-left-20 + = f.label :developers_can_push, "Developers can push", class: "label-light append-bottom-0" + %p.light.append-bottom-0 + Allow developers to push to this branch + = f.submit "Protect", class: "btn-create btn" + %hr + = render "branches_list" diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml index 48b3b5c9920..112b51712ef 100644 --- a/app/views/projects/triggers/_trigger.html.haml +++ b/app/views/projects/triggers/_trigger.html.haml @@ -1,7 +1,6 @@ %tr %td - .clearfix - %span.monospace= trigger.token + %span.monospace= trigger.token %td - if trigger.last_trigger_request @@ -9,6 +8,5 @@ - else Never - %td - .pull-right - = link_to 'Revoke', namespace_project_trigger_path(@project.namespace, @project, trigger), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-danger btn-sm btn-grouped" + %td.text-right + = link_to 'Revoke', namespace_project_trigger_path(@project.namespace, @project, trigger), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-warning btn-sm" diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml index bd346c4b8e6..f91885b216d 100644 --- a/app/views/projects/triggers/index.html.haml +++ b/app/views/projects/triggers/index.html.haml @@ -1,71 +1,70 @@ - page_title "Triggers" -%h3.page-title - Triggers -%p.light - Triggers can be used to force a rebuild of a specific branch or tag with an API call. +.row.prepend-top-default.append-bottom-default + .col-lg-3 + %h4.prepend-top-0 + = page_title + %p + Triggers can be used to force a rebuild of a specific branch or tag with an API call. + .col-lg-9 + %h5.prepend-top-0 + Your triggers + - if @triggers.any? + .table-responsive + %table.table + %thead + %th Token + %th Last used + %th + = render partial: 'trigger', collection: @triggers, as: :trigger + - else + %p.profile-settings-message.text-center.append-bottom-default + There are no triggers to use, add one by the button below. -%hr.clearfix + = form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create') do |f| + = f.submit "Add Trigger", class: 'btn btn-success' --if @triggers.any? - .table-holder - %table.table - %thead - %th Token - %th Last used - %th - = render partial: 'trigger', collection: @triggers, as: :trigger -- else - %h4 No triggers + %h5.prepend-top-default + Use CURL -= form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create'), html: { class: 'form-horizontal' } do |f| - .clearfix - = f.submit "Add Trigger", class: 'btn btn-success pull-right' + %p.light + Copy the token above and set your branch or tag name. This is the reference that will be rebuild. -%hr.clearfix --if @triggers.any? - %h3 - Use CURL + %pre + :plain + curl -X POST \ + -F token=TOKEN \ + -F ref=REF_NAME \ + #{builds_trigger_url(@project.id)} + %h5.prepend-top-default + Use .gitlab-ci.yml - %p.light - Copy the token above and set your branch or tag name. This is the reference that will be rebuild. + %p.light + Copy the snippet to + %i .gitlab-ci.yml + of dependent project. + At the end of your build it will trigger this project to rebuilt. + %pre + :plain + trigger: + type: deploy + script: + - "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}" + %h5.prepend-top-default + Pass build variables - %pre - :plain - curl -X POST \ - -F token=TOKEN \ - -F ref=REF_NAME \ - #{builds_trigger_url(@project.id)} - %h3 - Use .gitlab-ci.yml + %p.light + Add + %strong variables[VARIABLE]=VALUE + to API request. + The value of variable could then be used to distinguish triggered build from normal one. - %p.light - Copy the snippet to - %i .gitlab-ci.yml - of dependent project. - At the end of your build it will trigger this project to rebuilt. - - %pre - :plain - trigger: - type: deploy - script: - - "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}" - %h3 - Pass build variables - - %p.light - Add - %strong variables[VARIABLE]=VALUE - to API request. - The value of variable could then be used to distinguish triggered build from normal one. - - %pre - :plain - curl -X POST \ - -F token=TOKEN \ - -F "ref=REF_NAME" \ - -F "variables[RUN_NIGHTLY_BUILD]=true" \ - #{builds_trigger_url(@project.id)} + %pre.append-bottom-0 + :plain + curl -X POST \ + -F token=TOKEN \ + -F "ref=REF_NAME" \ + -F "variables[RUN_NIGHTLY_BUILD]=true" \ + #{builds_trigger_url(@project.id)} diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index 2c3fca439f3..2c378231237 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -2,97 +2,70 @@ - if @project %li{class: ("active" if @scope == 'blobs')} = link_to search_filter_path(scope: 'blobs') do - = icon('code fw') - %span - Code - %span.badge - = @search_results.blobs_count + Code + %span.badge + = @search_results.blobs_count %li{class: ("active" if @scope == 'issues')} = link_to search_filter_path(scope: 'issues') do - = icon('exclamation-circle fw') - %span - Issues - %span.badge - = @search_results.issues_count + Issues + %span.badge + = @search_results.issues_count %li{class: ("active" if @scope == 'merge_requests')} = link_to search_filter_path(scope: 'merge_requests') do - = icon('tasks fw') - %span - Merge requests - %span.badge - = @search_results.merge_requests_count + Merge requests + %span.badge + = @search_results.merge_requests_count %li{class: ("active" if @scope == 'milestones')} = link_to search_filter_path(scope: 'milestones') do - = icon('clock-o fw') - %span - Milestones - %span.badge - = @search_results.milestones_count + Milestones + %span.badge + = @search_results.milestones_count %li{class: ("active" if @scope == 'notes')} = link_to search_filter_path(scope: 'notes') do - = icon('comments fw') - %span - Comments - %span.badge - = @search_results.notes_count + Comments + %span.badge + = @search_results.notes_count %li{class: ("active" if @scope == 'wiki_blobs')} = link_to search_filter_path(scope: 'wiki_blobs') do - = icon('book fw') - %span - Wiki - %span.badge - = @search_results.wiki_blobs_count + Wiki + %span.badge + = @search_results.wiki_blobs_count %li{class: ("active" if @scope == 'commits')} = link_to search_filter_path(scope: 'commits') do - = icon('history fw') - %span - Commits - %span.badge - = @search_results.commits_count + Commits + %span.badge + = @search_results.commits_count - elsif @show_snippets %li{class: ("active" if @scope == 'snippet_blobs')} = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do - = icon('code fw') - %span - Snippet Contents - %span.badge - = @search_results.snippet_blobs_count + Snippet Contents + %span.badge + = @search_results.snippet_blobs_count %li{class: ("active" if @scope == 'snippet_titles')} = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do - = icon('book fw') - %span - Titles and Filenames - %span.badge - = @search_results.snippet_titles_count + Titles and Filenames + %span.badge + = @search_results.snippet_titles_count - else %li{class: ("active" if @scope == 'projects')} = link_to search_filter_path(scope: 'projects') do - = icon('bookmark fw') - %span - Projects - %span.badge - = @search_results.projects_count + Projects + %span.badge + = @search_results.projects_count %li{class: ("active" if @scope == 'issues')} = link_to search_filter_path(scope: 'issues') do - = icon('exclamation-circle fw') - %span - Issues - %span.badge - = @search_results.issues_count + Issues + %span.badge + = @search_results.issues_count %li{class: ("active" if @scope == 'merge_requests')} = link_to search_filter_path(scope: 'merge_requests') do - = icon('tasks fw') - %span - Merge requests - %span.badge - = @search_results.merge_requests_count + Merge requests + %span.badge + = @search_results.merge_requests_count %li{class: ("active" if @scope == 'milestones')} = link_to search_filter_path(scope: 'milestones') do - = icon('clock-o fw') - %span - Milestones - %span.badge - = @search_results.milestones_count - + Milestones + %span.badge + = @search_results.milestones_count diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml index 4ef544136a8..ef1c0296d49 100644 --- a/app/views/search/_filter.html.haml +++ b/app/views/search/_filter.html.haml @@ -1,47 +1,33 @@ -.dropdown.inline - %button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'} - %span.light Group: - - if @group.present? - %strong= @group.name - - else - Any - %b.caret - .dropdown-menu.dropdown-select.dropdown-menu-selectable - .dropdown-title - %span Filter results by group - %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} - = icon('times') - .dropdown-content - %ul - %li - = link_to search_filter_path(group_id: nil), class: ("is-active" if !params[:group_id].present?) do - Any - %li.divider - - current_user.authorized_groups.sort_by(&:name).each do |group| - %li - = link_to search_filter_path(group_id: group.id, project_id: nil), class: ("is-active" if params[:group_id] == group.id.to_s) do - = group.name +- if params[:group_id].present? + = hidden_field_tag :group_id, params[:group_id] +- if params[:project_id].present? + = hidden_field_tag :project_id, params[:project_id] +.dropdown + %button.dropdown-menu-toggle.btn.js-search-group-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Group:" } } + %span.dropdown-toggle-text + Group: + - if @group.present? + = @group.name + - else + Any + = icon("chevron-down") + .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-align-right + = dropdown_title("Filter results by group") + = dropdown_filter("Search groups") + = dropdown_content + = dropdown_loading -.dropdown.inline.prepend-left-10.project-filter - %button.dropdown-toggle.btn.btn-sm{type: 'button', 'data-toggle' => 'dropdown'} - %span.light Project: - - if @project.present? - %strong= @project.name_with_namespace - - else - Any - %b.caret - .dropdown-menu.dropdown-select.dropdown-menu-selectable - .dropdown-title - %span Filter results by project - %button.dropdown-title-button.dropdown-menu-close{aria: {label: "Close"}} - = icon('times') - .dropdown-content - %ul - %li - = link_to search_filter_path(project_id: nil), class: ("is-active" if !params[:project_id].present?) do - Any - %li.divider - - current_user.authorized_projects.sort_by(&:name_with_namespace).each do |project| - %li - = link_to search_filter_path(project_id: project.id, group_id: nil), class: ("is-active" if params[:project_id] == project.id.to_s) do - = project.name_with_namespace +.dropdown.project-filter + %button.dropdown-menu-toggle.btn.js-search-project-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Project:" } } + %span.dropdown-toggle-text + Project: + - if @project.present? + = @project.name_with_namespace + - else + Any + = icon("chevron-down") + .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-align-right + = dropdown_title("Filter results by project") + = dropdown_filter("Search projects") + = dropdown_content + = dropdown_loading diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml index a9dbc84da29..3139be1cd37 100644 --- a/app/views/search/_form.html.haml +++ b/app/views/search/_form.html.haml @@ -1,14 +1,15 @@ -= form_tag search_path, method: :get do |f| - = hidden_field_tag :project_id, params[:project_id] - = hidden_field_tag :group_id, params[:group_id] += form_tag search_path, method: :get, class: 'js-search-form' do |f| = hidden_field_tag :snippets, params[:snippets] = hidden_field_tag :scope, params[:scope] - .search-holder.clearfix - .input-group - = search_field_tag :search, params[:search], placeholder: "Search for projects, issues etc", class: "form-control search-text-input", id: "dashboard_search", autofocus: true, spellcheck: false - %span.input-group-btn - = button_tag 'Search', class: "btn btn-primary" + .search-holder + .search-field-holder + = search_field_tag :search, params[:search], placeholder: "Search for projects, issues etc", class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false + = icon("search", class: "search-icon") + %button.search-clear.js-search-clear{ class: ("hidden" if !params[:search].present?), type: "button", tabindex: "-1" } + = icon("times-circle") + %span.sr-only + Clear search - unless params[:snippets].eql? 'true' - %br = render 'filter' if current_user + = button_tag "Search", class: "btn btn-success btn-search" diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index 60df348891c..711337f308e 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -1,10 +1,8 @@ -- if @search_results.empty? +- if @search_objects.empty? = render partial: "search/results/empty" - else .gray-content-block - Search results for - %code - = @search_term + = search_entries_info(@search_objects, @scope, @search_term) - unless @show_snippets - if @project in project #{link_to @project.name_with_namespace, [@project.namespace.becomes(Namespace), @project]} @@ -15,12 +13,9 @@ .search-results - if @scope == 'projects' .term - = render 'shared/projects/list', projects: @objects + = render 'shared/projects/list', projects: @search_objects - else - = render partial: "search/results/#{@scope.singularize}", collection: @objects + = render partial: "search/results/#{@scope.singularize}", collection: @search_objects - if @scope != 'projects' - = paginate @objects, theme: 'gitlab' - -:javascript - $(".search-results .term").highlight("#{escape_javascript(params[:search])}"); + = paginate(@search_objects, theme: 'gitlab') diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml index 710f5613c81..640890fbe92 100644 --- a/app/views/search/results/_issue.html.haml +++ b/app/views/search/results/_issue.html.haml @@ -7,7 +7,7 @@ - if issue.description.present? .description.term = preserve do - = search_md_sanitize(markdown(issue.description, { project: issue.project })) + = search_md_sanitize(markdown(truncate(issue.description, length: 200, separator: " "), { project: issue.project })) %span.light #{issue.project.name_with_namespace} - if issue.closed? diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml index 34241cd8aad..b0fc60573f7 100644 --- a/app/views/shared/_confirm_modal.html.haml +++ b/app/views/shared/_confirm_modal.html.haml @@ -7,7 +7,7 @@ Confirmation required .modal-body - %p.cred.lead.js-confirm-text + %p.text-danger.js-confirm-text %p This action can lead to data loss. diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml index 57856031d6e..37dcf39c062 100644 --- a/app/views/shared/_file_highlight.html.haml +++ b/app/views/shared/_file_highlight.html.haml @@ -1,12 +1,13 @@ .file-content.code.js-syntax-highlight .line-numbers - if blob.data.present? + - link_icon = icon('link') - blob.data.each_line.each_with_index do |_, index| - offset = defined?(first_line_number) ? first_line_number : 1 - i = index + offset -# We're not using `link_to` because it is too slow once we get to thousands of lines. %a.diff-line-num{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i} - %i.fa.fa-link + = link_icon = i .blob-content{data: {blob_id: blob.id}} - = highlight(blob.name, blob.data) + = highlight(blob.name, blob.data, plain: blob.no_highlighting?) diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 04b834ba5d1..ed1b8a8da2a 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -166,5 +166,5 @@ new LabelsSelect(); new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}'); new Subscription('.subscription') - new Sidebar(); new DueDateSelect(); + sidebar = new Sidebar(); diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 3028491e5b6..0dff27f9654 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -69,13 +69,13 @@ = @user.location %ul.nav-links.center.user-profile-nav - %li.activity-tab + %li.js-activity-tab = link_to user_calendar_activities_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do Activity - %li.groups-tab + %li.js-groups-tab = link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do Groups - %li.contributed-tab + %li.js-contributed-tab = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do Contributed projects %li.projects-tab diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml index dc249155b92..4beb8746444 100644 --- a/app/views/votes/_votes_block.html.haml +++ b/app/views/votes/_votes_block.html.haml @@ -15,16 +15,16 @@ - if current_user :javascript - var get_emojis_url = "#{emojis_path}"; - var post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}"; - var noteable_type = "#{votable.class.name.underscore}"; - var noteable_id = "#{votable.id}"; - var aliases = #{AwardEmoji.aliases.to_json}; + var getEmojisUrl = "#{emojis_path}"; + var postEmojiUrl = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}"; + var noteableType = "#{votable.class.name.underscore}"; + var noteableId = "#{votable.id}"; + var unicodes = #{AwardEmoji.unicode.to_json}; - window.awards_handler = new AwardsHandler( - get_emojis_url, - post_emoji_url, - noteable_type, - noteable_id, - aliases + window.awardsHandler = new AwardsHandler( + getEmojisUrl, + postEmojiUrl, + noteableType, + noteableId, + unicodes ); diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb index 44b3145d50f..a3e16fa5212 100644 --- a/app/workers/repository_check/batch_worker.rb +++ b/app/workers/repository_check/batch_worker.rb @@ -33,8 +33,8 @@ module RepositoryCheck # has to sit and wait for this query to finish. def project_ids limit = 10_000 - never_checked_projects = Project.where('last_repository_check_at IS NULL').limit(limit). - pluck(:id) + never_checked_projects = Project.where('last_repository_check_at IS NULL AND created_at < ?', 24.hours.ago). + limit(limit).pluck(:id) old_check_projects = Project.where('last_repository_check_at < ?', 1.month.ago). reorder('last_repository_check_at ASC').limit(limit).pluck(:id) never_checked_projects + old_check_projects diff --git a/config/application.rb b/config/application.rb index 2e2ed48db07..b602e2b6168 100644 --- a/config/application.rb +++ b/config/application.rb @@ -32,7 +32,30 @@ module Gitlab config.encoding = "utf-8" # Configure sensitive parameters which will be filtered from the log file. - config.filter_parameters.push(:password, :password_confirmation, :private_token, :otp_attempt, :variables, :import_url) + # + # Parameters filtered: + # - Password (:password, :password_confirmation) + # - Private tokens (:private_token) + # - Two-factor tokens (:otp_attempt) + # - Repo/Project Import URLs (:import_url) + # - Build variables (:variables) + # - GitLab Pages SSL cert/key info (:certificate, :encrypted_key) + # - Webhook URLs (:hook) + # - Sentry DSN (:sentry_dsn) + # - Deploy keys (:key) + config.filter_parameters += %i( + certificate + encrypted_key + hook + import_url + key + otp_attempt + password + password_confirmation + private_token + sentry_dsn + variables + ) # Enable escaping HTML in JSON. config.active_support.escape_html_entities_in_json = true diff --git a/config/environments/development.rb b/config/environments/development.rb index 689694a3480..4f39016bfa4 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -36,7 +36,7 @@ Rails.application.configure do # For having correct urls in mails config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } # Open sent mails in browser - config.action_mailer.delivery_method = :letter_opener + config.action_mailer.delivery_method = :letter_opener_web # Don't make a mess when bootstrapping a development environment config.action_mailer.perform_deliveries = (ENV['BOOTSTRAP'] != '1') diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index d9c15f81404..07ce4b6d715 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -168,9 +168,9 @@ production: &base # once per hour you will have concurrent 'git fsck' jobs. repository_check_worker: cron: "20 * * * *" - # Send admin emails once a day + # Send admin emails once a week admin_email_worker: - cron: "0 0 * * *" + cron: "0 0 * * 0" # Remove outdated repository archives repository_archive_cache_worker: @@ -350,6 +350,8 @@ production: &base # - { name: 'github', # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', + # url: "https://github.com/", + # verify_ssl: true, # args: { scope: 'user:email' } } # - { name: 'bitbucket', # app_id: 'YOUR_APP_ID', diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 10c25044b75..8db2c05fe45 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -140,6 +140,30 @@ Settings.omniauth.cas3['session_duration'] ||= 8.hours Settings.omniauth['session_tickets'] ||= Settingslogic.new({}) Settings.omniauth.session_tickets['cas3'] = 'ticket' +# Fill out omniauth-gitlab settings. It is needed for easy set up GHE or GH by just specifying url. + +github_default_url = "https://github.com" +github_settings = Settings.omniauth['providers'].find { |provider| provider["name"] == "github" } + +if github_settings + # For compatibility with old config files (before 7.8) + # where people dont have url in github settings + if github_settings['url'].blank? + github_settings['url'] = github_default_url + end + + github_settings["args"] ||= Settingslogic.new({}) + + if github_settings["url"].include?(github_default_url) + github_settings["args"]["client_options"] = OmniAuth::Strategies::GitHub.default_options[:client_options] + else + github_settings["args"]["client_options"] = { + "site" => File.join(github_settings["url"], "api/v3"), + "authorize_url" => File.join(github_settings["url"], "login/oauth/authorize"), + "token_url" => File.join(github_settings["url"], "login/oauth/access_token") + } + end +end Settings['shared'] ||= Settingslogic.new({}) Settings.shared['path'] = File.expand_path(Settings.shared['path'] || "shared", Rails.root) @@ -245,7 +269,7 @@ Settings.cron_jobs['repository_check_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['repository_check_worker']['cron'] ||= '20 * * * *' Settings.cron_jobs['repository_check_worker']['job_class'] = 'RepositoryCheck::BatchWorker' Settings.cron_jobs['admin_email_worker'] ||= Settingslogic.new({}) -Settings.cron_jobs['admin_email_worker']['cron'] ||= '0 0 * * *' +Settings.cron_jobs['admin_email_worker']['cron'] ||= '0 0 * * 0' Settings.cron_jobs['admin_email_worker']['job_class'] = 'AdminEmailWorker' Settings.cron_jobs['repository_archive_cache_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['repository_archive_cache_worker']['cron'] ||= '0 * * * *' diff --git a/config/initializers/devise_async.rb b/config/initializers/devise_async.rb deleted file mode 100644 index 05a1852cdbd..00000000000 --- a/config/initializers/devise_async.rb +++ /dev/null @@ -1 +0,0 @@ -Devise::Async.backend = :sidekiq diff --git a/config/initializers/rack_attack.rb.example b/config/initializers/rack_attack.rb.example index b1bbcca1d61..30d05f16153 100644 --- a/config/initializers/rack_attack.rb.example +++ b/config/initializers/rack_attack.rb.example @@ -17,8 +17,9 @@ paths_to_be_protected = [ # Create one big regular expression that matches strings starting with any of # the paths_to_be_protected. paths_regex = Regexp.union(paths_to_be_protected.map { |path| /\A#{Regexp.escape(path)}/ }) +rack_attack_enabled = Gitlab.config.rack_attack.git_basic_auth['enabled'] -unless Rails.env.test? +unless Rails.env.test? || !rack_attack_enabled Rack::Attack.throttle('protected paths', limit: 10, period: 60.seconds) do |req| if req.post? && req.path =~ paths_regex req.ip diff --git a/config/initializers/rack_attack_git_basic_auth.rb b/config/initializers/rack_attack_git_basic_auth.rb index bbbfed68329..6a721826170 100644 --- a/config/initializers/rack_attack_git_basic_auth.rb +++ b/config/initializers/rack_attack_git_basic_auth.rb @@ -1,4 +1,6 @@ -unless Rails.env.test? +rack_attack_enabled = Gitlab.config.rack_attack.git_basic_auth['enabled'] + +unless Rails.env.test? || !rack_attack_enabled # Tell the Rack::Attack Rack middleware to maintain an IP blacklist. We will # update the blacklist from Grack::Auth#authenticate_user. Rack::Attack.blacklist('Git HTTP Basic Auth') do |req| diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index e87899b2d5c..74fef7cadfe 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -15,6 +15,9 @@ if Rails.env.production? Raven.configure do |config| config.dsn = current_application_settings.sentry_dsn config.release = Gitlab::REVISION + + # Sanitize fields based on those sanitized from Rails. + config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s) end end end diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 88cb859871c..599dabb9e50 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -22,7 +22,7 @@ else key: '_gitlab_session', secure: Gitlab.config.gitlab.https, httponly: true, - expire_after: Settings.gitlab['session_expire_delay'] * 60, + expires_in: Settings.gitlab['session_expire_delay'] * 60, path: (Rails.application.config.relative_url_root.nil?) ? '/' : Gitlab::Application.config.relative_url_root ) end diff --git a/config/routes.rb b/config/routes.rb index 99ce0118104..871997d01a2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -16,16 +16,18 @@ Rails.application.routes.draw do end end - # Make the built-in Rails routes available in development, otherwise they'd - # get swallowed by the `namespace/project` route matcher below. - # - # See https://git.io/va79N if Rails.env.development? + # Make the built-in Rails routes available in development, otherwise they'd + # get swallowed by the `namespace/project` route matcher below. + # + # See https://git.io/va79N get '/rails/mailers' => 'rails/mailers#index' get '/rails/mailers/:path' => 'rails/mailers#preview' get '/rails/info/properties' => 'rails/info#properties' get '/rails/info/routes' => 'rails/info#routes' get '/rails/info' => 'rails/info#index' + + mount LetterOpenerWeb::Engine, at: '/rails/letter_opener' end namespace :ci do @@ -218,8 +220,6 @@ Rails.application.routes.draw do resources :keys, only: [:show, :destroy] resources :identities, except: [:show] - delete 'stop_impersonation' => 'impersonation#destroy', on: :collection - member do get :projects get :keys @@ -229,12 +229,14 @@ Rails.application.routes.draw do put :unblock put :unlock put :confirm - post 'impersonate' => 'impersonation#create' + post :impersonate patch :disable_two_factor delete 'remove/:email_id', action: 'remove_email', as: 'remove_email' end end + resource :impersonation, only: :destroy + resources :abuse_reports, only: [:index, :destroy] resources :spam_logs, only: [:index, :destroy] @@ -716,6 +718,7 @@ Rails.application.routes.draw do post :toggle_subscription get :referenced_merge_requests get :related_branches + get :can_create_branch end collection do post :bulk_update diff --git a/db/migrate/20160413115152_add_token_to_web_hooks.rb b/db/migrate/20160413115152_add_token_to_web_hooks.rb new file mode 100644 index 00000000000..f04225068cd --- /dev/null +++ b/db/migrate/20160413115152_add_token_to_web_hooks.rb @@ -0,0 +1,5 @@ +class AddTokenToWebHooks < ActiveRecord::Migration + def change + add_column :web_hooks, :token, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index f7dfd296aea..40a295ca635 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1025,6 +1025,7 @@ ActiveRecord::Schema.define(version: 20160421130527) do t.boolean "enable_ssl_verification", default: true t.boolean "build_events", default: false, null: false t.boolean "wiki_page_events", default: false, null: false + t.string "token" end add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index 4b1788a9af0..5fb086b1dd9 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -8,7 +8,7 @@ This is one of new trends in Continuous Integration/Deployment to: 1. create application image, 1. run test against created image, -1. push image to remote registry, +1. push image to remote registry, 1. deploy server from pushed image It's also useful in case when your application already has the `Dockerfile` that can be used to create and test image: @@ -46,22 +46,22 @@ GitLab Runner then executes build scripts as `gitlab-runner` user. For more information how to install Docker on different systems checkout the [Supported installations](https://docs.docker.com/installation/). 3. Add `gitlab-runner` user to `docker` group: - + ```bash $ sudo usermod -aG docker gitlab-runner ``` 4. Verify that `gitlab-runner` has access to Docker: - + ```bash $ sudo -u gitlab-runner -H docker info ``` - + You can now verify that everything works by adding `docker info` to `.gitlab-ci.yml`: ```yaml before_script: - docker info - + build_image: script: - docker build -t my-docker-image . @@ -75,37 +75,80 @@ For more information please checkout [On Docker security: `docker` group conside ## 2. Use docker-in-docker executor -Second approach is to use special Docker image with all tools installed (`docker` and `docker-compose`) and run build script in context of that image in privileged mode. +The second approach is to use the special Docker image with all tools installed +(`docker` and `docker-compose`) and run the build script in context of that +image in privileged mode. + In order to do that follow the steps: 1. Install [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/#installation). -1. Register GitLab Runner from command line to use `docker` and `privileged` mode: +1. Register GitLab Runner from the command line to use `docker` and `privileged` + mode: ```bash - $ sudo gitlab-runner register -n \ + sudo gitlab-runner register -n \ --url https://gitlab.com/ci \ --token RUNNER_TOKEN \ --executor docker \ --description "My Docker Runner" \ - --docker-image "gitlab/dind:latest" \ + --docker-image "docker:latest" \ --docker-privileged ``` - - The above command will register new Runner to use special [gitlab/dind](https://registry.hub.docker.com/u/gitlab/dind/) image which is provided by GitLab Inc. - The image at the start runs Docker daemon in [docker-in-docker](https://blog.docker.com/2013/09/docker-can-now-run-within-docker/) mode. + + The above command will register a new Runner to use the special + `docker:latest` image which is provided by Docker. **Notice that it's using + the `privileged` mode to start the build and service containers.** If you + want to use [docker-in-docker] mode, you always have to use `privileged = true` + in your Docker containers. + + The above command will create a `config.toml` entry similar to this: + + ``` + [[runners]] + url = "https://gitlab.com/ci" + token = TOKEN + executor = "docker" + [runners.docker] + tls_verify = false + image = "docker:latest" + privileged = true + disable_cache = false + volumes = ["/cache"] + [runners.cache] + Insecure = false + ``` + + If you want to use the Shared Runners available on your GitLab CE/EE + installation in order to build Docker images, then make sure that your + Shared Runners configuration has the `privileged` mode set to `true`. 1. You can now use `docker` from build script: - + ```yaml + image: docker:latest + + services: + - docker:dind + before_script: - - docker info - - build_image: + - docker info + + build: + stage: build script: - - docker build -t my-docker-image . - - docker run my-docker-image /script/to/run/tests + - docker build -t my-docker-image . + - docker run my-docker-image /script/to/run/tests ``` -1. However, by enabling `--docker-privileged` you are effectively disables all security mechanisms of containers and exposing your host to privilege escalation which can lead to container breakout. -For more information, check out [Runtime privilege](https://docs.docker.com/reference/run/#runtime-privilege-linux-capabilities-and-lxc-configuration).
\ No newline at end of file +1. However, by enabling `--docker-privileged` you are effectively disabling all + the security mechanisms of containers and exposing your host to privilege + escalation which can lead to container breakout. + + For more information, check out the official Docker documentation on + [Runtime privilege and Linux capabilities][docker-cap]. + +An example project using this approach can be found here: https://gitlab.com/gitlab-examples/docker. + +[docker-in-docker]: https://blog.docker.com/2013/09/docker-can-now-run-within-docker/ +[docker-cap]: https://docs.docker.com/reference/run/#runtime-privilege-and-linux-capabilities diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index bd748f1b986..84212fb3c61 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -239,8 +239,8 @@ is specific to your project. Then create some service containers: ``` -docker run -d -n service-mysql mysql:latest -docker run -d -n service-postgres postgres:latest +docker run -d --name service-mysql mysql:latest +docker run -d --name service-postgres postgres:latest ``` This will create two service containers, named `service-mysql` and diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md index aeadd6a448e..db077927126 100644 --- a/doc/ci/examples/php.md +++ b/doc/ci/examples/php.md @@ -40,7 +40,7 @@ repository with the following content: #!/bin/bash # We need to install dependencies only for Docker -[[ ! -e /.dockerinit ]] && exit 0 +[[ ! -e /.dockerenv ]] && [[ ! -e /.dockerinit ]] && exit 0 set -xe diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index 9f7c1bfe6a0..79ed512aabb 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -85,6 +85,12 @@ curl -X POST \ In this case, the project with ID `9` will get rebuilt on `master` branch. +Alternatively, you can pass the `token` and `ref` arguments in the query string: + +```bash +curl -X POST \ + "https://gitlab.example.com/api/v3/projects/9/trigger/builds?token=TOKEN&ref=master" +``` ### Triggering a build within `.gitlab-ci.yml` diff --git a/doc/customization/libravatar.md b/doc/customization/libravatar.md index bd2c242afc2..c46ce2ee203 100644 --- a/doc/customization/libravatar.md +++ b/doc/customization/libravatar.md @@ -67,3 +67,16 @@ Run `sudo gitlab-ctl reconfigure` for changes to take effect. In order to use a different set other than `identicon`, replace `&d=identicon` portion of the URL with another supported set. For example, you can use `retro` set in which case the URL would look like: `plain_url: "http://cdn.libravatar.org/avatar/%{hash}?s=%{size}&d=retro"` + + +## Usage examples + +#### For Microsoft Office 365 + +If your users are Office 365-users, the "GetPersonaPhoto" service can be used. Note that this service requires login, so this use case is +most useful in a corporate installation, where all users have access to Office 365. + +```ruby +gitlab_rails['gravatar_plain_url'] = 'http://outlook.office365.com/owa/service.svc/s/GetPersonaPhoto?email=%{email}&size=HR120x120' +gitlab_rails['gravatar_ssl_url'] = 'https://outlook.office365.com/owa/service.svc/s/GetPersonaPhoto?email=%{email}&size=HR120x120' +``` diff --git a/doc/install/requirements.md b/doc/install/requirements.md index eb9fe5e1b1b..58f409746cd 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -142,4 +142,4 @@ On a very active server (10,000 active users) the Sidekiq process can use 1GB+ o - Firefox (Latest released version and [latest ESR version](https://www.mozilla.org/en-US/firefox/organizations/)) - Safari 7+ (known problem: required fields in html5 do not work) - Opera (Latest released version) -- Internet Explorer (IE) 10+ but please make sure that you have the `Compatibility View` mode disabled. +- Internet Explorer (IE) 11+ but please make sure that you have the `Compatibility View` mode disabled. diff --git a/doc/integration/github.md b/doc/integration/github.md index 1890edd7a4c..e7497e475c9 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -9,7 +9,9 @@ GitHub will generate an application ID and secret key for you to use. 1. Navigate to your individual user settings or an organization's settings, depending on how you want the application registered. It does not matter if the application is registered as an individual or an organization - that is entirely up to you. -1. Select "Applications" in the left menu. +1. Select "OAuth applications" in the left menu. + +1. If you already have applications listed, switch to the "Developer applications" tab. 1. Select "Register new application". @@ -60,12 +62,26 @@ GitHub will generate an application ID and secret key for you to use. For installation from source: + For GitHub.com: + ``` - { name: 'github', app_id: 'YOUR_APP_ID', app_secret: 'YOUR_APP_SECRET', args: { scope: 'user:email' } } ``` + + For GitHub Enterprise: + + ``` + - { name: 'github', app_id: 'YOUR_APP_ID', + app_secret: 'YOUR_APP_SECRET', + url: "https://github.example.com/", + args: { scope: 'user:email' } } + ``` + + __Replace `https://github.example.com/` with your GitHub URL.__ + 1. Change 'YOUR_APP_ID' to the client ID from the GitHub application page from step 7. 1. Change 'YOUR_APP_SECRET' to the client secret from the GitHub application page from step 7. diff --git a/doc/monitoring/performance/gitlab_configuration.md b/doc/monitoring/performance/gitlab_configuration.md index 90e99302210..771584268d9 100644 --- a/doc/monitoring/performance/gitlab_configuration.md +++ b/doc/monitoring/performance/gitlab_configuration.md @@ -37,4 +37,4 @@ Read more on: - [Introduction to GitLab Performance Monitoring](introduction.md) - [InfluxDB Configuration](influxdb_configuration.md) - [InfluxDB Schema](influxdb_schema.md) -- [Grafana Install/Configuration](grafana_configuration.md +- [Grafana Install/Configuration](grafana_configuration.md) diff --git a/doc/monitoring/performance/influxdb_configuration.md b/doc/monitoring/performance/influxdb_configuration.md index 63aa03985ef..c30cd2950d8 100644 --- a/doc/monitoring/performance/influxdb_configuration.md +++ b/doc/monitoring/performance/influxdb_configuration.md @@ -181,7 +181,7 @@ Read more on: - [Introduction to GitLab Performance Monitoring](introduction.md) - [GitLab Configuration](gitlab_configuration.md) - [InfluxDB Schema](influxdb_schema.md) -- [Grafana Install/Configuration](grafana_configuration.md +- [Grafana Install/Configuration](grafana_configuration.md) [influxdb-retention]: https://docs.influxdata.com/influxdb/v0.9/query_language/database_management/#retention-policy-management [influxdb documentation]: https://docs.influxdata.com/influxdb/v0.9/ diff --git a/doc/monitoring/performance/influxdb_schema.md b/doc/monitoring/performance/influxdb_schema.md index d31b3788f36..41861860b6d 100644 --- a/doc/monitoring/performance/influxdb_schema.md +++ b/doc/monitoring/performance/influxdb_schema.md @@ -85,4 +85,4 @@ Read more on: - [Introduction to GitLab Performance Monitoring](introduction.md) - [GitLab Configuration](gitlab_configuration.md) - [InfluxDB Configuration](influxdb_configuration.md) -- [Grafana Install/Configuration](grafana_configuration.md +- [Grafana Install/Configuration](grafana_configuration.md) diff --git a/doc/operations/sidekiq_memory_killer.md b/doc/operations/sidekiq_memory_killer.md index 811c2192a19..b5e78348989 100644 --- a/doc/operations/sidekiq_memory_killer.md +++ b/doc/operations/sidekiq_memory_killer.md @@ -36,5 +36,5 @@ The MemoryKiller is controlled using environment variables. Existing jobs get 30 seconds to finish. After that, the MemoryKiller tells Sidekiq to shut down, and an external supervision mechanism (e.g. Runit) must restart Sidekiq. -- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL`: defaults to 'SIGTERM'. The name of +- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL`: defaults to `SIGKILL`. The name of the final signal sent to the Sidekiq process when we want it to shut down. diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 4329ac30a1c..fa976134341 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -295,36 +295,49 @@ Deleting tmp directories...[DONE] ### Omnibus installations -We will assume that you have installed GitLab from an omnibus package and run -`sudo gitlab-ctl reconfigure` at least once. +This procedure assumes that: -First make sure your backup tar file is in `/var/opt/gitlab/backups` (or wherever `gitlab_rails['backup_path']` points to). +- You have installed the exact same version of GitLab Omnibus with which the + backup was created +- You have run `sudo gitlab-ctl reconfigure` at least once +- GitLab is running. If not, start it using `sudo gitlab-ctl start`. + +First make sure your backup tar file is in the backup directory described in the +`gitlab.rb` configuration `gitlab_rails['backup_path']`. The default is +`/var/opt/gitlab/backups`. ```shell sudo cp 1393513186_gitlab_backup.tar /var/opt/gitlab/backups/ ``` -Next, restore the backup by running the restore command. You need to specify the -timestamp of the backup you are restoring. +Stop the processes that are connected to the database. Leave the rest of GitLab +running: ```shell -# Stop processes that are connected to the database sudo gitlab-ctl stop unicorn sudo gitlab-ctl stop sidekiq +# Verify +sudo gitlab-ctl status +``` +Next, restore the backup, specifying the timestamp of the backup you wish to +restore: + +```shell # This command will overwrite the contents of your GitLab database! sudo gitlab-rake gitlab:backup:restore BACKUP=1393513186 +``` -# Start GitLab -sudo gitlab-ctl start +Restart and check GitLab: -# Check GitLab +```shell +sudo gitlab-ctl start sudo gitlab-rake gitlab:check SANITIZE=true ``` If there is a GitLab version mismatch between your backup tar file and the installed -version of GitLab, the restore command will abort with an error. Install a package for -the [required version](https://www.gitlab.com/downloads/archives/) and try again. +version of GitLab, the restore command will abort with an error. Install the +[correct GitLab version](https://www.gitlab.com/downloads/archives/) and try again. ## Configure cron to make daily backups diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index f446ed0a35b..60729316cde 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -47,7 +47,7 @@ sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`ca ```bash cd /home/git/gitlab-workhorse sudo -u git -H git fetch -sudo -u git -H git checkout `cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION` -b `cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION` +sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION` -b v`cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION` sudo -u git -H make ``` diff --git a/doc/workflow/merge_requests.md b/doc/workflow/merge_requests.md index 6d57b5d98cd..1b5718c91c1 100644 --- a/doc/workflow/merge_requests.md +++ b/doc/workflow/merge_requests.md @@ -12,9 +12,9 @@ Locate the section for your GitLab remote in the `.git/config` file. It looks li fetch = +refs/heads/*:refs/remotes/origin/* ``` -Now add the line `fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*` to this section. +Now add the line `fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*` to this section. -It should looks like this: +It should look like this: ``` [remote "origin"] @@ -43,7 +43,7 @@ $ git checkout origin/merge-requests/1 ![MR diff](merge_requests/merge_request_diff.png) -It you add `w=1` option to URL, you can see diff without whitespace changes. +If you click the "Hide whitespace changes" button, you can see the diff without whitespace changes. ![MR diff without whitespace](merge_requests/merge_request_diff_without_whitespace.png) diff --git a/doc/workflow/merge_requests/commit_compare.png b/doc/workflow/merge_requests/commit_compare.png Binary files differindex 46b3a56a59b..dfd7ee220f0 100644 --- a/doc/workflow/merge_requests/commit_compare.png +++ b/doc/workflow/merge_requests/commit_compare.png diff --git a/doc/workflow/merge_requests/merge_request_diff.png b/doc/workflow/merge_requests/merge_request_diff.png Binary files differindex ed08ae91bec..f368423c746 100644 --- a/doc/workflow/merge_requests/merge_request_diff.png +++ b/doc/workflow/merge_requests/merge_request_diff.png diff --git a/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png b/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png Binary files differindex 67d67a64d12..b2d03bb66f9 100644 --- a/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png +++ b/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png diff --git a/doc/workflow/shortcuts.png b/doc/workflow/shortcuts.png Binary files differindex 83e562d6929..beb6c53ec77 100644 --- a/doc/workflow/shortcuts.png +++ b/doc/workflow/shortcuts.png diff --git a/features/project/deploy_keys.feature b/features/project/deploy_keys.feature index 47cf774094f..960b4100ee5 100644 --- a/features/project/deploy_keys.feature +++ b/features/project/deploy_keys.feature @@ -21,7 +21,6 @@ Feature: Project Deploy Keys Scenario: I add new deploy key Given I visit project deploy keys page - When I click 'New Deploy Key' And I submit new deploy key Then I should be on deploy keys page And I should see newly created deploy key diff --git a/features/search.feature b/features/search.feature index 3cd52810e59..a946a836525 100644 --- a/features/search.feature +++ b/features/search.feature @@ -30,11 +30,13 @@ Feature: Search Then I should see "Foo" link in the search results And I should not see "Bar" link in the search results + @javascript Scenario: I should see project code I am looking for When I click project "Shop" link And I search for "rspec" Then I should see code results for project "Shop" + @javascript Scenario: I should see project issues And project has issues When I click project "Shop" link @@ -43,6 +45,7 @@ Feature: Search Then I should see "Foo" link in the search results And I should not see "Bar" link in the search results + @javascript Scenario: I should see project merge requests And project has merge requests When I click project "Shop" link @@ -51,6 +54,7 @@ Feature: Search Then I should see "Foo" link in the search results And I should not see "Bar" link in the search results + @javascript Scenario: I should see project milestones And project has milestones When I click project "Shop" link @@ -59,6 +63,7 @@ Feature: Search Then I should see "Foo" link in the search results And I should not see "Bar" link in the search results + @javascript Scenario: I should see Wiki blobs And project has Wiki content When I click project "Shop" link diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb index a4d6c9a1b8e..83b9ef48392 100644 --- a/features/steps/project/deploy_keys.rb +++ b/features/steps/project/deploy_keys.rb @@ -8,19 +8,19 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps end step 'I should see project deploy key' do - page.within '.enabled-keys' do + page.within '.deploy-keys' do expect(page).to have_content deploy_key.title end end step 'I should see other project deploy key' do - page.within '.available-keys' do + page.within '.deploy-keys' do expect(page).to have_content other_deploy_key.title end end step 'I should see public deploy key' do - page.within '.available-keys' do + page.within '.deploy-keys' do expect(page).to have_content public_deploy_key.title end end @@ -32,7 +32,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps step 'I submit new deploy key' do fill_in "deploy_key_title", with: "laptop" fill_in "deploy_key_key", with: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop" - click_button "Create" + click_button "Add key" end step 'I should be on deploy keys page' do @@ -40,7 +40,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps end step 'I should see newly created deploy key' do - page.within '.enabled-keys' do + page.within '.deploy-keys' do expect(page).to have_content(deploy_key.title) end end @@ -56,7 +56,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps end step 'I should only see the same deploy key once' do - page.within '.available-keys' do + page.within '.deploy-keys' do expect(page).to have_selector('ul li', count: 1) end end @@ -66,7 +66,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps end step 'I click attach deploy key' do - page.within '.available-keys' do + page.within '.deploy-keys' do click_link 'Enable' end end diff --git a/features/steps/project/hooks.rb b/features/steps/project/hooks.rb index 4994df589a7..b1ffe7f7b4c 100644 --- a/features/steps/project/hooks.rb +++ b/features/steps/project/hooks.rb @@ -48,12 +48,12 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps step 'I click test hook button' do stub_request(:post, @hook.url).to_return(status: 200) - click_link 'Test Hook' + click_link 'Test' end step 'I click test hook button with invalid URL' do stub_request(:post, @hook.url).to_raise(SocketError) - click_link 'Test Hook' + click_link 'Test' end step 'hook should be triggered' do diff --git a/features/steps/search.rb b/features/steps/search.rb index 0ad837ebe1d..f885baf8453 100644 --- a/features/steps/search.rb +++ b/features/steps/search.rb @@ -35,6 +35,7 @@ class Spinach::Features::Search < Spinach::FeatureSteps end step 'I click project "Shop" link' do + click_button 'Project' page.within '.project-filter' do click_link project.name_with_namespace end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 8aa08fd5acc..40928749481 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -24,8 +24,8 @@ module API def create_spam_log(project, current_user, attrs) params = attrs.merge({ - source_ip: env['REMOTE_ADDR'], - user_agent: env['HTTP_USER_AGENT'], + source_ip: client_ip(env), + user_agent: user_agent(env), noteable_type: 'Issue', via_api: true }) diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index 84b4d4cdd6d..132043cf3f7 100644 --- a/lib/api/milestones.rb +++ b/lib/api/milestones.rb @@ -105,7 +105,15 @@ module API authorize! :read_milestone, user_project @milestone = user_project.milestones.find(params[:milestone_id]) - present paginate(@milestone.issues), with: Entities::Issue, current_user: current_user + + finder_params = { + project_id: user_project.id, + milestone_title: @milestone.title, + state: 'all' + } + + issues = IssuesFinder.new(current_user, finder_params).execute + present paginate(issues), with: Entities::Issue, current_user: current_user end end diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index 22ce3c6a066..ce1bf0d26d2 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -11,6 +11,11 @@ module API end not_found! end + + def snippets_for_current_user + finder_params = { filter: :by_project, project: user_project } + SnippetsFinder.new.execute(current_user, finder_params) + end end # Get a project snippets @@ -20,7 +25,7 @@ module API # Example Request: # GET /projects/:id/snippets get ":id/snippets" do - present paginate(user_project.snippets), with: Entities::ProjectSnippet + present paginate(snippets_for_current_user), with: Entities::ProjectSnippet end # Get a project snippet @@ -31,7 +36,7 @@ module API # Example Request: # GET /projects/:id/snippets/:snippet_id get ":id/snippets/:snippet_id" do - @snippet = user_project.snippets.find(params[:snippet_id]) + @snippet = snippets_for_current_user.find(params[:snippet_id]) present @snippet, with: Entities::ProjectSnippet end @@ -73,7 +78,7 @@ module API # Example Request: # PUT /projects/:id/snippets/:snippet_id put ":id/snippets/:snippet_id" do - @snippet = user_project.snippets.find(params[:snippet_id]) + @snippet = snippets_for_current_user.find(params[:snippet_id]) authorize! :update_project_snippet, @snippet attrs = attributes_for_keys [:title, :file_name, :visibility_level] @@ -97,7 +102,7 @@ module API # DELETE /projects/:id/snippets/:snippet_id delete ":id/snippets/:snippet_id" do begin - @snippet = user_project.snippets.find(params[:snippet_id]) + @snippet = snippets_for_current_user.find(params[:snippet_id]) authorize! :update_project_snippet, @snippet @snippet.destroy rescue @@ -113,7 +118,7 @@ module API # Example Request: # GET /projects/:id/snippets/:snippet_id/raw get ":id/snippets/:snippet_id/raw" do - @snippet = user_project.snippets.find(params[:snippet_id]) + @snippet = snippets_for_current_user.find(params[:snippet_id]) env['api.format'] = :txt content_type 'text/plain' diff --git a/lib/award_emoji.rb b/lib/award_emoji.rb index 5f8ff01b0a9..b1aecc2e671 100644 --- a/lib/award_emoji.rb +++ b/lib/award_emoji.rb @@ -52,6 +52,10 @@ class AwardEmoji end end + def self.unicode + @unicode ||= emojis.map {|key, value| { key => emojis[key]["unicode"] } }.inject(:merge!) + end + def self.aliases @aliases ||= begin json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json' ) diff --git a/lib/gitlab/akismet_helper.rb b/lib/gitlab/akismet_helper.rb index b366c89889e..04676fdb748 100644 --- a/lib/gitlab/akismet_helper.rb +++ b/lib/gitlab/akismet_helper.rb @@ -9,14 +9,22 @@ module Gitlab Gitlab.config.gitlab.url) end + def client_ip(env) + env['action_dispatch.remote_ip'].to_s + end + + def user_agent(env) + env['HTTP_USER_AGENT'] + end + def check_for_spam?(project, user) akismet_enabled? && !project.team.member?(user) end def is_spam?(environment, user, text) client = akismet_client - ip_address = environment['REMOTE_ADDR'] - user_agent = environment['HTTP_USER_AGENT'] + ip_address = client_ip(environment) + user_agent = user_agent(environment) params = { type: 'comment', diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb index 74d1529e1ff..67988ea3460 100644 --- a/lib/gitlab/github_import/client.rb +++ b/lib/gitlab/github_import/client.rb @@ -7,12 +7,19 @@ module Gitlab @client = ::OAuth2::Client.new( config.app_id, config.app_secret, - github_options + github_options.merge(ssl: { verify: config['verify_ssl'] }) ) if access_token ::Octokit.auto_paginate = true - @api = ::Octokit::Client.new(access_token: access_token) + + @api = ::Octokit::Client.new( + access_token: access_token, + api_endpoint: github_options[:site], + connection_options: { + ssl: { verify: config['verify_ssl'] } + } + ) end end @@ -42,11 +49,11 @@ module Gitlab private def config - Gitlab.config.omniauth.providers.find{|provider| provider.name == "github"} + Gitlab.config.omniauth.providers.find { |provider| provider.name == "github" } end def github_options - OmniAuth::Strategies::GitHub.default_options[:client_options].to_h.symbolize_keys + config["args"]["client_options"].deep_symbolize_keys end end end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 5ebaad6ca6e..ab900b641c4 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -6,6 +6,7 @@ module Gitlab gon.default_issues_tracker = Project.new.default_issue_tracker.to_param gon.max_file_size = current_application_settings.max_attachment_size gon.relative_url_root = Gitlab.config.gitlab.relative_url_root + gon.shortcuts_path = help_shortcuts_path gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class if current_user diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb index cac76442321..280120b0f9e 100644 --- a/lib/gitlab/highlight.rb +++ b/lib/gitlab/highlight.rb @@ -1,7 +1,8 @@ module Gitlab class Highlight - def self.highlight(blob_name, blob_content, nowrap: true) - new(blob_name, blob_content, nowrap: nowrap).highlight(blob_content, continue: false) + def self.highlight(blob_name, blob_content, nowrap: true, plain: false) + new(blob_name, blob_content, nowrap: nowrap). + highlight(blob_content, continue: false, plain: plain) end def self.highlight_lines(repository, ref, file_name) @@ -17,8 +18,12 @@ module Gitlab @lexer = Rouge::Lexer.guess(filename: blob_name, source: blob_content).new rescue Rouge::Lexers::PlainText end - def highlight(text, continue: true) - @formatter.format(@lexer.lex(text, continue: continue)).html_safe + def highlight(text, continue: true, plain: false) + if plain + @formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe + else + @formatter.format(@lexer.lex(text, continue: continue)).html_safe + end rescue @formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe end diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb index f6fab0fe22d..394411a56c2 100644 --- a/lib/gitlab/import_export/project_tree_saver.rb +++ b/lib/gitlab/import_export/project_tree_saver.rb @@ -24,6 +24,7 @@ module Gitlab false end + # TODO remove magic keyword and move it to a shared config def project_filename "project.json" end diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb index 994fc4bea5a..315ad88ee0c 100644 --- a/lib/gitlab/import_export/repo_restorer.rb +++ b/lib/gitlab/import_export/repo_restorer.rb @@ -11,7 +11,7 @@ module Gitlab def restore return false unless File.exists?(@path) - # Move repos dir to 'repositories.old' dir + # Move repos dir to 'repositories.old' dir FileUtils.mkdir_p(repos_path) FileUtils.mkdir_p(path_to_repo) diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab index d95e7023d2e..31b00ff128a 100755 --- a/lib/support/init.d/gitlab +++ b/lib/support/init.d/gitlab @@ -173,7 +173,7 @@ check_stale_pids(){ fi fi if [ "$hpid" != "0" ] && [ "$gitlab_workhorse_status" != "0" ]; then - echo "Removing stale gitlab-workhorse pid. This is most likely caused by gitlab-workhorse crashing the last time it ran." + echo "Removing stale GitLab Workhorse pid. This is most likely caused by GitLab Workhorse crashing the last time it ran." if ! rm "$gitlab_workhorse_pid_path"; then echo "Unable to remove stale pid, exiting" exit 1 @@ -208,7 +208,7 @@ start_gitlab() { echo "Starting GitLab Sidekiq" fi if [ "$gitlab_workhorse_status" != "0" ]; then - echo "Starting gitlab-workhorse" + echo "Starting GitLab Workhorse" fi if [ "$mail_room_enabled" = true ] && [ "$mail_room_status" != "0" ]; then echo "Starting GitLab MailRoom" @@ -232,7 +232,7 @@ start_gitlab() { fi if [ "$gitlab_workhorse_status" = "0" ]; then - echo "The gitlab-workhorse is already running with pid $spid, not restarting" + echo "The GitLab Workhorse is already running with pid $spid, not restarting" else # No need to remove a socket, gitlab-workhorse does this itself. # Because gitlab-workhorse has multiple executables we need to fix @@ -271,7 +271,7 @@ stop_gitlab() { RAILS_ENV=$RAILS_ENV bin/background_jobs stop fi if [ "$gitlab_workhorse_status" = "0" ]; then - echo "Shutting down gitlab-workhorse" + echo "Shutting down GitLab Workhorse" kill -- $(cat $gitlab_workhorse_pid_path) fi if [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; then @@ -320,9 +320,9 @@ print_status() { printf "The GitLab Sidekiq job dispatcher is \033[31mnot running\033[0m.\n" fi if [ "$gitlab_workhorse_status" = "0" ]; then - echo "The gitlab-workhorse with pid $hpid is running." + echo "The GitLab Workhorse with pid $hpid is running." else - printf "The gitlab-workhorse is \033[31mnot running\033[0m.\n" + printf "The GitLab Workhorse is \033[31mnot running\033[0m.\n" fi if [ "$mail_room_enabled" = true ]; then if [ "$mail_room_status" = "0" ]; then diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index 1324e4cd267..d521de28e8a 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -61,7 +61,8 @@ server { error_page 422 /422.html; error_page 500 /500.html; error_page 502 /502.html; - location ~ ^/(404|422|500|502)\.html$ { + error_page 503 /503.html; + location ~ ^/(404|422|500|502|503)\.html$ { root /home/git/gitlab/public; internal; } diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index af6ea9ed706..bf014b56cf6 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -105,7 +105,8 @@ server { error_page 422 /422.html; error_page 500 /500.html; error_page 502 /502.html; - location ~ ^/(404|422|500|502)\.html$ { + error_page 503 /503.html; + location ~ ^/(404|422|500|502|503)\.html$ { root /home/git/gitlab/public; internal; } diff --git a/public/503.html b/public/503.html new file mode 100644 index 00000000000..6ab1185658d --- /dev/null +++ b/public/503.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<html> +<head> + <title>GitLab is not responding (503)</title> + <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="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo"/><br /> + 503 + </h1> + <h3>Whoops, GitLab is currently unavailable.</h3> + <hr/> + <p>Try refreshing the page, or going back and attempting the action again.</p> + <p>Please contact your GitLab administrator if this problem persists.</p> +</body> +</html> diff --git a/spec/controllers/admin/impersonation_controller_spec.rb b/spec/controllers/admin/impersonation_controller_spec.rb deleted file mode 100644 index d7a7ba1c5b6..00000000000 --- a/spec/controllers/admin/impersonation_controller_spec.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'spec_helper' - -describe Admin::ImpersonationController do - let(:admin) { create(:admin) } - - before do - sign_in(admin) - end - - describe 'CREATE #impersonation when blocked' do - let(:blocked_user) { create(:user, state: :blocked) } - - it 'does not allow impersonation' do - post :create, id: blocked_user.username - - expect(flash[:alert]).to eq 'You cannot impersonate a blocked user' - end - end -end diff --git a/spec/controllers/admin/impersonations_controller_spec.rb b/spec/controllers/admin/impersonations_controller_spec.rb new file mode 100644 index 00000000000..eb82476b179 --- /dev/null +++ b/spec/controllers/admin/impersonations_controller_spec.rb @@ -0,0 +1,95 @@ +require 'spec_helper' + +describe Admin::ImpersonationsController do + let(:impersonator) { create(:admin) } + let(:user) { create(:user) } + + describe "DELETE destroy" do + context "when not signed in" do + it "redirects to the sign in page" do + delete :destroy + + expect(response).to redirect_to(new_user_session_path) + end + end + + context "when signed in" do + before do + sign_in(user) + end + + context "when not impersonating" do + it "responds with status 404" do + delete :destroy + + expect(response.status).to eq(404) + end + + it "doesn't sign us in" do + delete :destroy + + expect(warden.user).to eq(user) + end + end + + context "when impersonating" do + before do + session[:impersonator_id] = impersonator.id + end + + context "when the impersonator is not admin (anymore)" do + before do + impersonator.admin = false + impersonator.save + end + + it "responds with status 404" do + delete :destroy + + expect(response.status).to eq(404) + end + + it "doesn't sign us in as the impersonator" do + delete :destroy + + expect(warden.user).to eq(user) + end + end + + context "when the impersonator is admin" do + context "when the impersonator is blocked" do + before do + impersonator.block! + end + + it "responds with status 404" do + delete :destroy + + expect(response.status).to eq(404) + end + + it "doesn't sign us in as the impersonator" do + delete :destroy + + expect(warden.user).to eq(user) + end + end + + context "when the impersonator is not blocked" do + it "redirects to the impersonated user's page" do + delete :destroy + + expect(response).to redirect_to(admin_user_path(user)) + end + + it "signs us in as the impersonator" do + delete :destroy + + expect(warden.user).to eq(impersonator) + end + end + end + end + end + end +end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 9ef8ba1b097..ce2a62ae1fd 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -2,9 +2,10 @@ require 'spec_helper' describe Admin::UsersController do let(:user) { create(:user) } + let(:admin) { create(:admin) } before do - sign_in(create(:admin)) + sign_in(admin) end describe 'DELETE #user with projects' do @@ -112,4 +113,50 @@ describe Admin::UsersController do patch :disable_two_factor, id: user.to_param end end + + describe "POST impersonate" do + context "when the user is blocked" do + before do + user.block! + end + + it "shows a notice" do + post :impersonate, id: user.username + + expect(flash[:alert]).to eq("You cannot impersonate a blocked user") + end + + it "doesn't sign us in as the user" do + post :impersonate, id: user.username + + expect(warden.user).to eq(admin) + end + end + + context "when the user is not blocked" do + it "stores the impersonator in the session" do + post :impersonate, id: user.username + + expect(session[:impersonator_id]).to eq(admin.id) + end + + it "signs us in as the user" do + post :impersonate, id: user.username + + expect(warden.user).to eq(user) + end + + it "redirects to root" do + post :impersonate, id: user.username + + expect(response).to redirect_to(root_path) + end + + it "shows a notice" do + post :impersonate, id: user.username + + expect(flash[:alert]).to eq("You are now impersonating #{user.username}") + end + end + end end diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index bbf8adef534..bcc713dce2a 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -22,6 +22,8 @@ describe Import::GithubController do token = "asdasd12345" allow_any_instance_of(Gitlab::GithubImport::Client). to receive(:get_token).and_return(token) + allow_any_instance_of(Gitlab::GithubImport::Client). + to receive(:github_options).and_return({}) stub_omniauth_provider('github') get :callback diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index d6e4cd71ce6..2b2ad3b9412 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -40,6 +40,45 @@ describe Projects::IssuesController do end end + describe 'PUT #update' do + context 'when moving issue to another private project' do + let(:another_project) { create(:project, :private) } + + before do + sign_in(user) + project.team << [user, :developer] + end + + context 'when user has access to move issue' do + before { another_project.team << [user, :reporter] } + + it 'moves issue to another project' do + move_issue + + expect(response).to have_http_status :found + expect(another_project.issues).to_not be_empty + end + end + + context 'when user does not have access to move issue' do + it 'responds with 404' do + move_issue + + expect(response).to have_http_status :not_found + end + end + + def move_issue + put :update, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: issue.iid, + issue: { title: 'New title' }, + move_to_project_id: another_project.id + end + end + end + describe 'Confidential Issues' do let(:project) { create(:project_empty_repo, :public) } let(:assignee) { create(:assignee) } diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb index 94dd935a039..3195fb3ddcc 100644 --- a/spec/factories/project_hooks.rb +++ b/spec/factories/project_hooks.rb @@ -1,5 +1,9 @@ FactoryGirl.define do factory :project_hook do url { FFaker::Internet.uri('http') } + + trait :token do + token { SecureRandom.hex(10) } + end end end diff --git a/spec/features/dashboard/label_filter_spec.rb b/spec/features/dashboard/label_filter_spec.rb new file mode 100644 index 00000000000..24e83d44010 --- /dev/null +++ b/spec/features/dashboard/label_filter_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe 'Dashboard > label filter', feature: true, js: true do + let(:user) { create(:user) } + let(:project) { create(:project, name: 'test', namespace: user.namespace) } + let(:project2) { create(:project, name: 'test2', path: 'test2', namespace: user.namespace) } + let(:label) { create(:label, title: 'bug', color: '#ff0000') } + let(:label2) { create(:label, title: 'bug') } + + before do + project.labels << label + project2.labels << label2 + + login_as(user) + visit issues_dashboard_path + end + + context 'duplicate labels' do + it 'should remove duplicate labels' do + page.within('.labels-filter') do + click_button 'Label' + end + + page.within('.dropdown-menu-labels') do + expect(page).to have_selector('.dropdown-content a', text: 'bug', count: 1) + end + end + end +end diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb index 9219b767547..16e188d2a8a 100644 --- a/spec/features/issues/new_branch_button_spec.rb +++ b/spec/features/issues/new_branch_button_spec.rb @@ -11,10 +11,10 @@ feature 'Start new branch from an issue', feature: true do login_as(user) end - it 'shown the new branch button', js: false do + it 'shows the new branch button', js: true do visit namespace_project_issue_path(project.namespace, project, issue) - expect(page).to have_link "New Branch" + expect(page).to have_css('#new-branch .available') end context "when there is a referenced merge request" do @@ -34,16 +34,17 @@ feature 'Start new branch from an issue', feature: true do end it "hides the new branch button", js: true do - expect(page).not_to have_link "New Branch" + expect(page).not_to have_css('#new-branch .available') expect(page).to have_content /1 Related Merge Request/ end end end context "for visiters" do - it 'no button is shown', js: false do + it 'no button is shown', js: true do visit namespace_project_issue_path(project.namespace, project, issue) - expect(page).not_to have_link "New Branch" + + expect(page).not_to have_css('#new-branch') end end end diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index 00b60bd0e75..e296078bad8 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -30,4 +30,14 @@ feature 'Create New Merge Request', feature: true, js: true do expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch' end + + context 'when target project cannot be viewed by the current user' do + it 'does not leak the private project name & namespace' do + private_project = create(:project, :private) + + visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_project_id: private_project.id }) + + expect(page).not_to have_content private_project.to_reference + end + end end diff --git a/spec/features/merge_requests/toggle_whitespace_changes.rb b/spec/features/merge_requests/toggle_whitespace_changes.rb new file mode 100644 index 00000000000..0f98737b700 --- /dev/null +++ b/spec/features/merge_requests/toggle_whitespace_changes.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +feature 'Toggle Whitespace Changes', js: true, feature: true do + before do + login_as :admin + merge_request = create(:merge_request) + project = merge_request.source_project + visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'has a button to toggle whitespace changes' do + expect(page).to have_content 'Hide whitespace changes' + end + + describe 'clicking "Hide whitespace changes" button' do + it 'toggles the "Hide whitespace changes" button' do + click_link 'Hide whitespace changes' + + expect(page).to have_content 'Show whitespace changes' + end + end +end diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb new file mode 100644 index 00000000000..c2c7acff3e8 --- /dev/null +++ b/spec/features/milestone_spec.rb @@ -0,0 +1,35 @@ +require 'rails_helper' + +feature 'Milestone', feature: true do + include WaitForAjax + + let(:project) { create(:project, :public) } + let(:user) { create(:user) } + let(:milestone) { create(:milestone, project: project, title: 8.7) } + + before do + project.team << [user, :master] + login_as(user) + end + + feature 'Create a milestone' do + scenario 'should show an informative message for a new issue' do + visit new_namespace_project_milestone_path(project.namespace, project) + page.within '.milestone-form' do + fill_in "milestone_title", with: '8.7' + end + find('input[name="commit"]').click + + expect(find('.alert-success')).to have_content('Assign some issues to this milestone.') + end + end + + feature 'Open a milestone with closed issues' do + scenario 'should show an informative message' do + create(:issue, title: "Bugfix1", project: project, milestone: milestone, state: "closed") + visit namespace_project_milestone_path(project.namespace, project, milestone) + + expect(find('.alert-success')).to have_content('All issues for this milestone are closed. You may close this milestone now.') + end + end +end diff --git a/spec/features/project/shortcuts_spec.rb b/spec/features/project/shortcuts_spec.rb new file mode 100644 index 00000000000..2595c4181e5 --- /dev/null +++ b/spec/features/project/shortcuts_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +feature 'Project shortcuts', feature: true do + let(:project) { create(:project) } + let(:user) { create(:user) } + + describe 'On a project', js: true do + before do + project.team << [user, :master] + login_as user + visit namespace_project_path(project.namespace, project) + end + + describe 'pressing "i"' do + it 'redirects to new issue page' do + find('body').native.send_key('i') + expect(page).to have_content('New Issue') + end + end + end +end diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb new file mode 100644 index 00000000000..40ba0bdc115 --- /dev/null +++ b/spec/features/projects/commit/builds_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +feature 'project commit builds' do + given(:project) { create(:project) } + + background do + user = create(:user) + project.team << [user, :master] + login_as(user) + end + + context 'when no builds triggered yet' do + background do + create(:ci_commit, project: project, + sha: project.commit.sha, + ref: 'master') + end + + scenario 'user views commit builds page' do + visit builds_namespace_project_commit_path(project.namespace, + project, project.commit.sha) + + + expect(page).to have_content('Builds') + end + end +end diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb new file mode 100644 index 00000000000..7e6eef65873 --- /dev/null +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' + +feature 'Projects > Wiki > User creates wiki page', feature: true do + let(:user) { create(:user) } + + background do + project.team << [user, :master] + login_as(user) + + visit namespace_project_path(project.namespace, project) + click_link 'Wiki' + end + + context 'in the user namespace' do + let(:project) { create(:project, namespace: user.namespace) } + + context 'when wiki is empty' do + scenario 'directly from the wiki home page' do + fill_in :wiki_content, with: 'My awesome wiki!' + click_button 'Create page' + + expect(page).to have_content('Home') + expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content('My awesome wiki!') + end + end + + context 'when wiki is not empty' do + before do + WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute + end + + scenario 'via the "new wiki page" page', js: true do + click_link 'New Page' + + fill_in :new_wiki_path, with: 'foo' + click_button 'Create Page' + + fill_in :wiki_content, with: 'My awesome wiki!' + click_button 'Create page' + + expect(page).to have_content('Foo') + expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content('My awesome wiki!') + end + end + end + + context 'in a group namespace' do + let(:project) { create(:project, namespace: create(:group, :public)) } + + context 'when wiki is empty' do + scenario 'directly from the wiki home page' do + fill_in :wiki_content, with: 'My awesome wiki!' + click_button 'Create page' + + expect(page).to have_content('Home') + expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content('My awesome wiki!') + end + end + + context 'when wiki is not empty' do + before do + WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute + end + + scenario 'via the "new wiki page" page', js: true do + click_link 'New Page' + + fill_in :new_wiki_path, with: 'foo' + click_button 'Create Page' + + fill_in :wiki_content, with: 'My awesome wiki!' + click_button 'Create page' + + expect(page).to have_content('Foo') + expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content('My awesome wiki!') + end + end + end +end diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb new file mode 100644 index 00000000000..ef82d2375dd --- /dev/null +++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +feature 'Projects > Wiki > User updates wiki page', feature: true do + let(:user) { create(:user) } + + background do + project.team << [user, :master] + login_as(user) + + visit namespace_project_path(project.namespace, project) + WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute + click_link 'Wiki' + end + + context 'in the user namespace' do + let(:project) { create(:project, namespace: user.namespace) } + + scenario 'the home page' do + click_link 'Edit' + + fill_in :wiki_content, with: 'My awesome wiki!' + click_button 'Save changes' + + expect(page).to have_content('Home') + expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content('My awesome wiki!') + end + end + + context 'in a group namespace' do + let(:project) { create(:project, namespace: create(:group, :public)) } + + scenario 'the home page' do + click_link 'Edit' + + fill_in :wiki_content, with: 'My awesome wiki!' + click_button 'Save changes' + + expect(page).to have_content('Home') + expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content('My awesome wiki!') + end + end +end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 782c0bfe666..9dd0378d165 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -104,6 +104,33 @@ feature 'Project', feature: true do end end + describe 'project title' do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + let(:project2) { create(:project, namespace: user.namespace, path: 'test') } + let(:issue) { create(:issue, project: project) } + + context 'on issues page', js: true do + before do + login_with(user) + project.team.add_user(user, Gitlab::Access::MASTER) + project2.team.add_user(user, Gitlab::Access::MASTER) + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it 'click toggle and show dropdown' do + find('.js-projects-dropdown-toggle').click + expect(page).to have_css('.dropdown-menu-projects .dropdown-content li', count: 2) + + page.within '.dropdown-menu-projects' do + click_link project.name_with_namespace + end + + expect(page).to have_content project.name + end + end + end + def remove_with_confirm(button_text, confirm_with) click_button button_text fill_in 'confirm_name_input', with: confirm_with diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 248e004ba6e..3354f529295 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -68,12 +68,12 @@ describe 'Dashboard Todos', feature: true do describe 'completing last todo from last page', js: true do it 'redirects to the previous page' do visit dashboard_todos_path(page: 2) - expect(page).to have_content(Todo.first.body) + expect(page).to have_css("#todo_#{Todo.last.id}") click_link('Done') expect(current_path).to eq dashboard_todos_path - expect(page).to have_content(Todo.last.body) + expect(page).to have_css("#todo_#{Todo.first.id}") end end end diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index 543593cf389..bffe2c18b6f 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -30,6 +30,18 @@ describe IssuesHelper do expect(url_for_project_issues).to eq "" end + it 'returns an empty string if project_url is invalid' do + expect(project).to receive_message_chain('issues_tracker.project_url') { 'javascript:alert("foo");' } + + expect(url_for_project_issues(project)).to eq '' + end + + it 'returns an empty string if project_path is invalid' do + expect(project).to receive_message_chain('issues_tracker.project_path') { 'javascript:alert("foo");' } + + expect(url_for_project_issues(project, only_path: true)).to eq '' + end + describe "when external tracker was enabled and then config removed" do before do @project = ext_project @@ -68,6 +80,18 @@ describe IssuesHelper do expect(url_for_issue(issue.iid)).to eq "" end + it 'returns an empty string if issue_url is invalid' do + expect(project).to receive_message_chain('issues_tracker.issue_url') { 'javascript:alert("foo");' } + + expect(url_for_issue(issue.iid, project)).to eq '' + end + + it 'returns an empty string if issue_path is invalid' do + expect(project).to receive_message_chain('issues_tracker.issue_path') { 'javascript:alert("foo");' } + + expect(url_for_issue(issue.iid, project, only_path: true)).to eq '' + end + describe "when external tracker was enabled and then config removed" do before do @project = ext_project @@ -105,6 +129,18 @@ describe IssuesHelper do expect(url_for_new_issue).to eq "" end + it 'returns an empty string if issue_url is invalid' do + expect(project).to receive_message_chain('issues_tracker.new_issue_url') { 'javascript:alert("foo");' } + + expect(url_for_new_issue(project)).to eq '' + end + + it 'returns an empty string if issue_path is invalid' do + expect(project).to receive_message_chain('issues_tracker.new_issue_path') { 'javascript:alert("foo");' } + + expect(url_for_new_issue(project, only_path: true)).to eq '' + end + describe "when external tracker was enabled and then config removed" do before do @project = ext_project diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index 39042ff7e91..501f150cfda 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -11,13 +11,13 @@ 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}"><span class="[\w\s\-]*has-tooltip".*</span></a>} + expect(link_to_label(label)).to match %r{<a href="/#{@project.to_reference}/issues\?label_name%5B%5D=#{label.name}"><span class="[\w\s\-]*has-tooltip".*</span></a>} end end context 'without @project set' do it "uses the label's project" do - expect(link_to_label(label)).to match %r{<a href="/#{label.project.to_reference}/issues\?label_name=#{label.name}">.*</a>} + expect(link_to_label(label)).to match %r{<a href="/#{label.project.to_reference}/issues\?label_name%5B%5D=#{label.name}">.*</a>} end end @@ -25,7 +25,7 @@ describe LabelsHelper do let(:another_project) { double('project', namespace: 'foo3', to_param: 'bar3') } it 'links to merge requests page' do - expect(link_to_label(label, project: another_project)).to match %r{<a href="/foo3/bar3/issues\?label_name=#{label.name}">.*</a>} + expect(link_to_label(label, project: another_project)).to match %r{<a href="/foo3/bar3/issues\?label_name%5B%5D=#{label.name}">.*</a>} end end @@ -33,7 +33,7 @@ describe LabelsHelper do ['issue', :issue, 'merge_request', :merge_request].each do |type| context "set to #{type}" do it 'links to correct page' do - expect(link_to_label(label, type: type)).to match %r{<a href="/#{label.project.to_reference}/#{type.to_s.pluralize}\?label_name=#{label.name}">.*</a>} + expect(link_to_label(label, type: type)).to match %r{<a href="/#{label.project.to_reference}/#{type.to_s.pluralize}\?label_name%5B%5D=#{label.name}">.*</a>} end end end diff --git a/spec/lib/gitlab/akismet_helper_spec.rb b/spec/lib/gitlab/akismet_helper_spec.rb index 9858935180a..53f5d6c5c80 100644 --- a/spec/lib/gitlab/akismet_helper_spec.rb +++ b/spec/lib/gitlab/akismet_helper_spec.rb @@ -24,7 +24,7 @@ describe Gitlab::AkismetHelper, type: :helper do describe '#is_spam?' do it 'returns true for spam' do environment = { - 'REMOTE_ADDR' => '127.0.0.1', + 'action_dispatch.remote_ip' => '127.0.0.1', 'HTTP_USER_AGENT' => 'Test User Agent' } diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb index 49d8cdf4314..7c21cbe96d9 100644 --- a/spec/lib/gitlab/github_import/client_spec.rb +++ b/spec/lib/gitlab/github_import/client_spec.rb @@ -2,15 +2,49 @@ require 'spec_helper' describe Gitlab::GithubImport::Client, lib: true do let(:token) { '123456' } - let(:client) { Gitlab::GithubImport::Client.new(token) } + let(:github_provider) { Settingslogic.new('app_id' => 'asd123', 'app_secret' => 'asd123', 'name' => 'github', 'args' => { 'client_options' => {} }) } + + subject(:client) { described_class.new(token) } before do - Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "github") + allow(Gitlab.config.omniauth).to receive(:providers).and_return([github_provider]) end - it 'all OAuth2 client options are symbols' do + it 'convert OAuth2 client options to symbols' do client.client.options.keys.each do |key| expect(key).to be_kind_of(Symbol) end end + + it 'does not crash (e.g. Settingslogic::MissingSetting) when verify_ssl config is not present' do + expect { client.api }.not_to raise_error + end + + context 'allow SSL verification to be configurable on API' do + before do + github_provider['verify_ssl'] = false + end + + it 'uses supplied value' do + expect(client.client.options[:connection_opts][:ssl]).to eq({ verify: false }) + expect(client.api.connection_options[:ssl]).to eq({ verify: false }) + end + end + + context 'when provider does not specity an API endpoint' do + it 'uses GitHub root API endpoint' do + expect(client.api.api_endpoint).to eq 'https://api.github.com/' + end + end + + context 'when provider specify a custom API endpoint' do + before do + github_provider['args']['client_options']['site'] = 'https://github.company.com/' + end + + it 'uses the custom API endpoint' do + expect(OmniAuth::Strategies::GitHub).not_to receive(:default_options) + expect(client.api.api_endpoint).to eq 'https://github.company.com/' + end + end end diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 8a923dde467..5c1b9af2c13 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -21,7 +21,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do labels: [label], snippets: [snippet], releases: [release] - ) + ) end let!(:ci_commit) { create(:ci_commit, project: project, sha: merge_request.last_commit.id, ref: merge_request.source_branch, statuses: [commit_status]) } let!(:milestone) { create(:milestone, title: "Milestone v1.2", project: project) } diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index 82c18aaa01a..a747aa08447 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -158,97 +158,123 @@ describe Ci::Commit, models: true do stub_ci_commit_yaml_file(YAML.dump(yaml)) end - it 'properly creates builds' do - expect(create_builds).to be_truthy - expect(commit.builds.pluck(:name)).to contain_exactly('build') - expect(commit.builds.pluck(:status)).to contain_exactly('pending') - commit.builds.running_or_pending.each(&:success) - - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') - commit.builds.running_or_pending.each(&:success) - - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') - commit.builds.running_or_pending.each(&:success) - - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending') - commit.builds.running_or_pending.each(&:success) - - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success') - commit.reload - expect(commit.status).to eq('success') + context 'when builds are successful' do + it 'properly creates builds' do + expect(create_builds).to be_truthy + expect(commit.builds.pluck(:name)).to contain_exactly('build') + expect(commit.builds.pluck(:status)).to contain_exactly('pending') + commit.builds.running_or_pending.each(&:success) + + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') + commit.builds.running_or_pending.each(&:success) + + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') + commit.builds.running_or_pending.each(&:success) + + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending') + commit.builds.running_or_pending.each(&:success) + + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success') + commit.reload + expect(commit.status).to eq('success') + end end - it 'properly creates builds when test fails' do - expect(create_builds).to be_truthy - expect(commit.builds.pluck(:name)).to contain_exactly('build') - expect(commit.builds.pluck(:status)).to contain_exactly('pending') - commit.builds.running_or_pending.each(&:success) + context 'when test job fails' do + it 'properly creates builds' do + expect(create_builds).to be_truthy + expect(commit.builds.pluck(:name)).to contain_exactly('build') + expect(commit.builds.pluck(:status)).to contain_exactly('pending') + commit.builds.running_or_pending.each(&:success) - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') - commit.builds.running_or_pending.each(&:drop) + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') + commit.builds.running_or_pending.each(&:drop) - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') - commit.builds.running_or_pending.each(&:success) + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') + commit.builds.running_or_pending.each(&:success) - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending') - commit.builds.running_or_pending.each(&:success) + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending') + commit.builds.running_or_pending.each(&:success) - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success') - commit.reload - expect(commit.status).to eq('failed') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success') + commit.reload + expect(commit.status).to eq('failed') + end end - it 'properly creates builds when test and test_failure fails' do - expect(create_builds).to be_truthy - expect(commit.builds.pluck(:name)).to contain_exactly('build') - expect(commit.builds.pluck(:status)).to contain_exactly('pending') - commit.builds.running_or_pending.each(&:success) + context 'when test and test_failure jobs fail' do + it 'properly creates builds' do + expect(create_builds).to be_truthy + expect(commit.builds.pluck(:name)).to contain_exactly('build') + expect(commit.builds.pluck(:status)).to contain_exactly('pending') + commit.builds.running_or_pending.each(&:success) + + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') + commit.builds.running_or_pending.each(&:drop) + + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') + commit.builds.running_or_pending.each(&:drop) + + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending') + commit.builds.running_or_pending.each(&:success) + + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success') + commit.reload + expect(commit.status).to eq('failed') + end + end - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') - commit.builds.running_or_pending.each(&:drop) + context 'when deploy job fails' do + it 'properly creates builds' do + expect(create_builds).to be_truthy + expect(commit.builds.pluck(:name)).to contain_exactly('build') + expect(commit.builds.pluck(:status)).to contain_exactly('pending') + commit.builds.running_or_pending.each(&:success) - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending') - commit.builds.running_or_pending.each(&:drop) + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') + commit.builds.running_or_pending.each(&:success) - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending') - commit.builds.running_or_pending.each(&:success) + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') + commit.builds.running_or_pending.each(&:drop) - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success') - commit.reload - expect(commit.status).to eq('failed') - end + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending') + commit.builds.running_or_pending.each(&:success) - it 'properly creates builds when deploy fails' do - expect(create_builds).to be_truthy - expect(commit.builds.pluck(:name)).to contain_exactly('build') - expect(commit.builds.pluck(:status)).to contain_exactly('pending') - commit.builds.running_or_pending.each(&:success) + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success') + commit.reload + expect(commit.status).to eq('failed') + end + end - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') - commit.builds.running_or_pending.each(&:success) + context 'when build is canceled in the second stage' do + it 'does not schedule builds after build has been canceled' do + expect(create_builds).to be_truthy + expect(commit.builds.pluck(:name)).to contain_exactly('build') + expect(commit.builds.pluck(:status)).to contain_exactly('pending') + commit.builds.running_or_pending.each(&:success) - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending') - commit.builds.running_or_pending.each(&:drop) + expect(commit.builds.running_or_pending).to_not be_empty - expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup') - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending') - commit.builds.running_or_pending.each(&:success) + expect(commit.builds.pluck(:name)).to contain_exactly('build', 'test') + expect(commit.builds.pluck(:status)).to contain_exactly('success', 'pending') + commit.builds.running_or_pending.each(&:cancel) - expect(commit.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success') - commit.reload - expect(commit.status).to eq('failed') + expect(commit.builds.running_or_pending).to be_empty + expect(commit.reload.status).to eq('canceled') + end end end end diff --git a/spec/models/concerns/statuseable_spec.rb b/spec/models/concerns/statuseable_spec.rb index dacbd3034c0..8e0a2a2cbde 100644 --- a/spec/models/concerns/statuseable_spec.rb +++ b/spec/models/concerns/statuseable_spec.rb @@ -61,9 +61,35 @@ describe Statuseable do let(:statuses) do [create(type, status: :success), create(type, status: :canceled)] end + + it { is_expected.to eq 'canceled' } + end + + context 'one failed and one canceled' do + let(:statuses) do + [create(type, status: :failed), create(type, status: :canceled)] + end + it { is_expected.to eq 'failed' } end + context 'one failed but allowed to fail and one canceled' do + let(:statuses) do + [create(type, status: :failed, allow_failure: true), + create(type, status: :canceled)] + end + + it { is_expected.to eq 'canceled' } + end + + context 'one running one canceled' do + let(:statuses) do + [create(type, status: :running), create(type, status: :canceled)] + end + + it { is_expected.to eq 'running' } + end + context 'all canceled' do let(:statuses) do [create(type, status: :canceled), create(type, status: :canceled)] diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 89909c2bcd7..0c3cd13f399 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -30,32 +30,29 @@ describe Event, models: true do it { is_expected.to respond_to(:commits) } end + describe 'Callbacks' do + describe 'after_create :reset_project_activity' do + let(:project) { create(:project) } + + context "project's last activity was less than 5 minutes ago" do + it 'does not update project.last_activity_at if it has been touched less than 5 minutes ago' do + create_event(project, project.owner) + project.update_column(:last_activity_at, 5.minutes.ago) + project_last_activity_at = project.last_activity_at + + create_event(project, project.owner) + + expect(project.last_activity_at).to eq(project_last_activity_at) + end + end + end + end + describe "Push event" do before do project = create(:project) @user = project.owner - - data = { - before: Gitlab::Git::BLANK_SHA, - after: "0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e", - ref: "refs/heads/master", - user_id: @user.id, - user_name: @user.name, - repository: { - name: project.name, - url: "localhost/rubinius", - description: "", - homepage: "localhost/rubinius", - private: true - } - } - - @event = Event.create( - project: project, - action: Event::PUSHED, - data: data, - author_id: @user.id - ) + @event = create_event(project, @user) end it { expect(@event.push?).to be_truthy } @@ -143,4 +140,28 @@ describe Event, models: true do it { is_expected.to eq([event2]) } end end + + def create_event(project, user, attrs = {}) + data = { + before: Gitlab::Git::BLANK_SHA, + after: "0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e", + ref: "refs/heads/master", + user_id: user.id, + user_name: user.name, + repository: { + name: project.name, + url: "localhost/rubinius", + description: "", + homepage: "localhost/rubinius", + private: true + } + } + + Event.create({ + project: project, + action: Event::PUSHED, + data: data, + author_id: user.id + }.merge(attrs)) + end end diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index 04bc2dcfb16..37a27d73aab 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -43,51 +43,65 @@ describe WebHook, models: true do end describe "execute" do + let(:project) { create(:project) } + let(:project_hook) { create(:project_hook) } + before(:each) do - @project_hook = create(:project_hook) - @project = create(:project) - @project.hooks << [@project_hook] + project.hooks << [project_hook] @data = { before: 'oldrev', after: 'newrev', ref: 'ref' } - WebMock.stub_request(:post, @project_hook.url) + WebMock.stub_request(:post, project_hook.url) + end + + context 'when token is defined' do + let(:project_hook) { create(:project_hook, :token) } + + it 'POSTs to the webhook URL' do + project_hook.execute(@data, 'push_hooks') + expect(WebMock).to have_requested(:post, project_hook.url).with( + headers: { 'Content-Type' => 'application/json', + 'X-Gitlab-Event' => 'Push Hook', + 'X-Gitlab-Token' => project_hook.token } + ).once + end end it "POSTs to the webhook URL" do - @project_hook.execute(@data, 'push_hooks') - expect(WebMock).to have_requested(:post, @project_hook.url).with( - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Push Hook' } + project_hook.execute(@data, 'push_hooks') + expect(WebMock).to have_requested(:post, project_hook.url).with( + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Push Hook' } ).once end it "POSTs the data as JSON" do - @project_hook.execute(@data, 'push_hooks') - expect(WebMock).to have_requested(:post, @project_hook.url).with( - headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Push Hook' } + project_hook.execute(@data, 'push_hooks') + expect(WebMock).to have_requested(:post, project_hook.url).with( + headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Push Hook' } ).once end it "catches exceptions" do expect(WebHook).to receive(:post).and_raise("Some HTTP Post error") - expect { @project_hook.execute(@data, 'push_hooks') }.to raise_error(RuntimeError) + expect { project_hook.execute(@data, 'push_hooks') }.to raise_error(RuntimeError) end it "handles SSL exceptions" do expect(WebHook).to receive(:post).and_raise(OpenSSL::SSL::SSLError.new('SSL error')) - expect(@project_hook.execute(@data, 'push_hooks')).to eq([false, 'SSL error']) + expect(project_hook.execute(@data, 'push_hooks')).to eq([false, 'SSL error']) end it "handles 200 status code" do - WebMock.stub_request(:post, @project_hook.url).to_return(status: 200, body: "Success") + WebMock.stub_request(:post, project_hook.url).to_return(status: 200, body: "Success") - expect(@project_hook.execute(@data, 'push_hooks')).to eq([true, 'Success']) + expect(project_hook.execute(@data, 'push_hooks')).to eq([true, 'Success']) end it "handles 2xx status codes" do - WebMock.stub_request(:post, @project_hook.url).to_return(status: 201, body: "Success") + WebMock.stub_request(:post, project_hook.url).to_return(status: 201, body: "Success") - expect(@project_hook.execute(@data, 'push_hooks')).to eq([true, 'Success']) + expect(project_hook.execute(@data, 'push_hooks')).to eq([true, 'Success']) end end end diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb index 31b2c90122d..e771f35811e 100644 --- a/spec/models/project_services/bamboo_service_spec.rb +++ b/spec/models/project_services/bamboo_service_spec.rb @@ -27,86 +27,51 @@ describe BambooService, models: true do end describe 'Validations' do - describe '#bamboo_url' do - it 'does not validate the presence of bamboo_url if service is not active' do - bamboo_service = service - bamboo_service.active = false - - expect(bamboo_service).not_to validate_presence_of(:bamboo_url) - end - - it 'validates the presence of bamboo_url if service is active' do - bamboo_service = service - bamboo_service.active = true - - expect(bamboo_service).to validate_presence_of(:bamboo_url) - end - end + subject { service } - describe '#build_key' do - it 'does not validate the presence of build_key if service is not active' do - bamboo_service = service - bamboo_service.active = false + context 'when service is active' do + before { subject.active = true } - expect(bamboo_service).not_to validate_presence_of(:build_key) - end + it { is_expected.to validate_presence_of(:build_key) } + it { is_expected.to validate_presence_of(:bamboo_url) } + it_behaves_like 'issue tracker service URL attribute', :bamboo_url - it 'validates the presence of build_key if service is active' do - bamboo_service = service - bamboo_service.active = true + describe '#username' do + it 'does not validate the presence of username if password is nil' do + subject.password = nil - expect(bamboo_service).to validate_presence_of(:build_key) - end - end + expect(subject).not_to validate_presence_of(:username) + end - describe '#username' do - it 'does not validate the presence of username if service is not active' do - bamboo_service = service - bamboo_service.active = false + it 'validates the presence of username if password is present' do + subject.password = 'secret' - expect(bamboo_service).not_to validate_presence_of(:username) + expect(subject).to validate_presence_of(:username) + end end - it 'does not validate the presence of username if username is nil' do - bamboo_service = service - bamboo_service.active = true - bamboo_service.password = nil + describe '#password' do + it 'does not validate the presence of password if username is nil' do + subject.username = nil - expect(bamboo_service).not_to validate_presence_of(:username) - end + expect(subject).not_to validate_presence_of(:password) + end - it 'validates the presence of username if service is active and username is present' do - bamboo_service = service - bamboo_service.active = true - bamboo_service.password = 'secret' + it 'validates the presence of password if username is present' do + subject.username = 'john' - expect(bamboo_service).to validate_presence_of(:username) + expect(subject).to validate_presence_of(:password) + end end end - describe '#password' do - it 'does not validate the presence of password if service is not active' do - bamboo_service = service - bamboo_service.active = false - - expect(bamboo_service).not_to validate_presence_of(:password) - end - - it 'does not validate the presence of password if username is nil' do - bamboo_service = service - bamboo_service.active = true - bamboo_service.username = nil - - expect(bamboo_service).not_to validate_presence_of(:password) - end - - it 'validates the presence of password if service is active and username is present' do - bamboo_service = service - bamboo_service.active = true - bamboo_service.username = 'john' + context 'when service is inactive' do + before { subject.active = false } - expect(bamboo_service).to validate_presence_of(:password) - end + it { is_expected.not_to validate_presence_of(:build_key) } + it { is_expected.not_to validate_presence_of(:bamboo_url) } + it { is_expected.not_to validate_presence_of(:username) } + it { is_expected.not_to validate_presence_of(:password) } end end diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb index 88cd624877a..60364df2015 100644 --- a/spec/models/project_services/buildkite_service_spec.rb +++ b/spec/models/project_services/buildkite_service_spec.rb @@ -26,6 +26,23 @@ describe BuildkiteService, models: true do it { is_expected.to have_one :service_hook } end + describe 'Validations' do + context 'when service is active' do + before { subject.active = true } + + it { is_expected.to validate_presence_of(:project_url) } + it { is_expected.to validate_presence_of(:token) } + it_behaves_like 'issue tracker service URL attribute', :project_url + end + + context 'when service is inactive' do + before { subject.active = false } + + it { is_expected.not_to validate_presence_of(:project_url) } + it { is_expected.not_to validate_presence_of(:token) } + end + end + describe 'commits methods' do before do @project = Project.new diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb index 7c23c2efccd..236df8f047d 100644 --- a/spec/models/project_services/builds_email_service_spec.rb +++ b/spec/models/project_services/builds_email_service_spec.rb @@ -1,76 +1,71 @@ require 'spec_helper' describe BuildsEmailService do - let(:build) { create(:ci_build) } - let(:data) { Gitlab::BuildDataBuilder.build(build) } - let!(:project) { create(:project, :public, ci_id: 1) } - let(:service) { described_class.new(project: project, active: true) } + let(:data) { Gitlab::BuildDataBuilder.build(create(:ci_build)) } + + describe 'Validations' do + context 'when service is active' do + before { subject.active = true } + + it { is_expected.to validate_presence_of(:recipients) } + + context 'when pusher is added' do + before { subject.add_pusher = true } + + it { is_expected.not_to validate_presence_of(:recipients) } + end + end + + context 'when service is inactive' do + before { subject.active = false } + + it { is_expected.not_to validate_presence_of(:recipients) } + end + end describe '#execute' do it 'sends email' do - service.recipients = 'test@gitlab.com' + subject.recipients = 'test@gitlab.com' data[:build_status] = 'failed' + expect(BuildEmailWorker).to receive(:perform_async) - service.execute(data) + + subject.execute(data) end it 'does not send email with succeeded build and notify_only_broken_builds on' do - expect(service).to receive(:notify_only_broken_builds).and_return(true) + expect(subject).to receive(:notify_only_broken_builds).and_return(true) data[:build_status] = 'success' + expect(BuildEmailWorker).not_to receive(:perform_async) - service.execute(data) + + subject.execute(data) end it 'does not send email with failed build and build_allow_failure is true' do data[:build_status] = 'failed' data[:build_allow_failure] = true + expect(BuildEmailWorker).not_to receive(:perform_async) - service.execute(data) + + subject.execute(data) end it 'does not send email with unknown build status' do data[:build_status] = 'foo' - expect(BuildEmailWorker).not_to receive(:perform_async) - service.execute(data) - end - it 'does not send email when recipients list is empty' do - service.recipients = ' ,, ' - data[:build_status] = 'failed' expect(BuildEmailWorker).not_to receive(:perform_async) - service.execute(data) - end - end - - describe 'validations' do - - context 'when pusher is not added' do - before { service.add_pusher = false } - - it 'does not allow empty recipient input' do - service.recipients = '' - expect(service.valid?).to be false - end - - it 'does allow non-empty recipient input' do - service.recipients = 'test@example.com' - expect(service.valid?).to be true - end + subject.execute(data) end - context 'when pusher is added' do - before { service.add_pusher = true } + it 'does not send email when recipients list is empty' do + subject.recipients = ' ,, ' + data[:build_status] = 'failed' - it 'does allow empty recipient input' do - service.recipients = '' - expect(service.valid?).to be true - end + expect(BuildEmailWorker).not_to receive(:perform_async) - it 'does allow non-empty recipient input' do - service.recipients = 'test@example.com' - expect(service.valid?).to be true - end + subject.execute(data) end end end diff --git a/spec/models/project_services/campfire_service_spec.rb b/spec/models/project_services/campfire_service_spec.rb new file mode 100644 index 00000000000..3e6da42803b --- /dev/null +++ b/spec/models/project_services/campfire_service_spec.rb @@ -0,0 +1,42 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'spec_helper' + +describe CampfireService, models: true do + describe 'Associations' do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe 'Validations' do + context 'when service is active' do + before { subject.active = true } + + it { is_expected.to validate_presence_of(:token) } + end + + context 'when service is inactive' do + before { subject.active = false } + + it { is_expected.not_to validate_presence_of(:token) } + end + end +end diff --git a/spec/models/project_services/custom_issue_tracker_service_spec.rb b/spec/models/project_services/custom_issue_tracker_service_spec.rb new file mode 100644 index 00000000000..ff976f8ec59 --- /dev/null +++ b/spec/models/project_services/custom_issue_tracker_service_spec.rb @@ -0,0 +1,49 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'spec_helper' + +describe CustomIssueTrackerService, models: true do + describe 'Associations' do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe 'Validations' do + context 'when service is active' do + before { subject.active = true } + + it { is_expected.to validate_presence_of(:project_url) } + it { is_expected.to validate_presence_of(:issues_url) } + it { is_expected.to validate_presence_of(:new_issue_url) } + it_behaves_like 'issue tracker service URL attribute', :project_url + it_behaves_like 'issue tracker service URL attribute', :issues_url + it_behaves_like 'issue tracker service URL attribute', :new_issue_url + end + + context 'when service is inactive' do + before { subject.active = false } + + it { is_expected.not_to validate_presence_of(:project_url) } + it { is_expected.not_to validate_presence_of(:issues_url) } + it { is_expected.not_to validate_presence_of(:new_issue_url) } + end + end +end diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb index a2cf68a9e38..3a8e67438fc 100644 --- a/spec/models/project_services/drone_ci_service_spec.rb +++ b/spec/models/project_services/drone_ci_service_spec.rb @@ -28,25 +28,18 @@ describe DroneCiService, models: true do describe 'validations' do context 'active' do - before { allow(subject).to receive(:activated?).and_return(true) } + before { subject.active = true } it { is_expected.to validate_presence_of(:token) } it { is_expected.to validate_presence_of(:drone_url) } - it { is_expected.to allow_value('ewf9843kdnfdfs89234n').for(:token) } - it { is_expected.to allow_value('http://ci.example.com').for(:drone_url) } - it { is_expected.not_to allow_value('this is not url').for(:drone_url) } - it { is_expected.not_to allow_value('http//noturl').for(:drone_url) } - it { is_expected.not_to allow_value('ftp://ci.example.com').for(:drone_url) } + it_behaves_like 'issue tracker service URL attribute', :drone_url end context 'inactive' do - before { allow(subject).to receive(:activated?).and_return(false) } + before { subject.active = false } it { is_expected.not_to validate_presence_of(:token) } it { is_expected.not_to validate_presence_of(:drone_url) } - it { is_expected.to allow_value('ewf9843kdnfdfs89234n').for(:token) } - it { is_expected.to allow_value('http://drone.example.com').for(:drone_url) } - it { is_expected.to allow_value('ftp://drone.example.com').for(:drone_url) } end end diff --git a/spec/models/project_services/emails_on_push_service_spec.rb b/spec/models/project_services/emails_on_push_service_spec.rb new file mode 100644 index 00000000000..e6f78898c82 --- /dev/null +++ b/spec/models/project_services/emails_on_push_service_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe EmailsOnPushService do + describe 'Validations' do + context 'when service is active' do + before { subject.active = true } + + it { is_expected.to validate_presence_of(:recipients) } + end + + context 'when service is inactive' do + before { subject.active = false } + + it { is_expected.not_to validate_presence_of(:recipients) } + end + end +end diff --git a/spec/models/external_wiki_service_spec.rb b/spec/models/project_services/external_wiki_service_spec.rb index d37978720bf..5fe5ea7d2df 100644 --- a/spec/models/external_wiki_service_spec.rb +++ b/spec/models/project_services/external_wiki_service_spec.rb @@ -28,13 +28,18 @@ describe ExternalWikiService, models: true do it { should have_one :service_hook } end - describe "Validations" do - context "active" do - before do - subject.active = true - end + describe 'Validations' do + context 'when service is active' do + before { subject.active = true } + + it { is_expected.to validate_presence_of(:external_wiki_url) } + it_behaves_like 'issue tracker service URL attribute', :external_wiki_url + end + + context 'when service is inactive' do + before { subject.active = false } - it { should validate_presence_of :external_wiki_url } + it { is_expected.not_to validate_presence_of(:external_wiki_url) } end end diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb index ff7fbcaa004..b7e627e6518 100644 --- a/spec/models/project_services/flowdock_service_spec.rb +++ b/spec/models/project_services/flowdock_service_spec.rb @@ -26,6 +26,20 @@ describe FlowdockService, models: true do it { is_expected.to have_one :service_hook } end + describe 'Validations' do + context 'when service is active' do + before { subject.active = true } + + it { is_expected.to validate_presence_of(:token) } + end + + context 'when service is inactive' do + before { subject.active = false } + + it { is_expected.not_to validate_presence_of(:token) } + end + end + describe "Execute" do let(:user) { create(:user) } let(:project) { create(:project) } diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb index ecb3ccb1673..a08f1ac229f 100644 --- a/spec/models/project_services/gemnasium_service_spec.rb +++ b/spec/models/project_services/gemnasium_service_spec.rb @@ -26,6 +26,22 @@ describe GemnasiumService, models: true do it { is_expected.to have_one :service_hook } end + describe 'Validations' do + context 'when service is active' do + before { subject.active = true } + + it { is_expected.to validate_presence_of(:token) } + it { is_expected.to validate_presence_of(:api_key) } + end + + context 'when service is inactive' do + before { subject.active = false } + + it { is_expected.not_to validate_presence_of(:token) } + it { is_expected.not_to validate_presence_of(:api_key) } + end + end + describe "Execute" do let(:user) { create(:user) } let(:project) { create(:project) } diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb index 3518dbd1728..7a1f106d6e3 100644 --- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb +++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb @@ -26,6 +26,20 @@ describe GitlabIssueTrackerService, models: true do it { is_expected.to have_one :service_hook } end + describe 'Validations' do + context 'when service is active' do + subject { described_class.new(project: create(:project), active: true) } + + it { is_expected.to validate_presence_of(:issues_url) } + it_behaves_like 'issue tracker service URL attribute', :issues_url + end + + context 'when service is inactive' do + subject { described_class.new(project: create(:project), active: false) } + + it { is_expected.not_to validate_presence_of(:issues_url) } + end + end describe 'project and issue urls' do let(:project) { create(:project) } diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index d878162a220..6fb5cad5011 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -26,6 +26,20 @@ describe HipchatService, models: true do it { is_expected.to have_one :service_hook } end + describe 'Validations' do + context 'when service is active' do + before { subject.active = true } + + it { is_expected.to validate_presence_of(:token) } + end + + context 'when service is inactive' do + before { subject.active = false } + + it { is_expected.not_to validate_presence_of(:token) } + end + end + describe "Execute" do let(:hipchat) { HipchatService.new } let(:user) { create(:user, username: 'username') } diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb index b783b1a576e..4ee022a5171 100644 --- a/spec/models/project_services/irker_service_spec.rb +++ b/spec/models/project_services/irker_service_spec.rb @@ -29,14 +29,16 @@ describe IrkerService, models: true do end describe 'Validations' do - before do - subject.active = true - subject.properties['recipients'] = _recipients + context 'when service is active' do + before { subject.active = true } + + it { is_expected.to validate_presence_of(:recipients) } end - context 'active' do - let(:_recipients) { nil } - it { should validate_presence_of :recipients } + context 'when service is inactive' do + before { subject.active = false } + + it { is_expected.not_to validate_presence_of(:recipients) } end end diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 2f8193170ae..5309cfb99ff 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -26,6 +26,30 @@ describe JiraService, models: true do it { is_expected.to have_one :service_hook } end + describe 'Validations' do + context 'when service is active' do + before { subject.active = true } + + it { is_expected.to validate_presence_of(:api_url) } + it { is_expected.to validate_presence_of(:project_url) } + it { is_expected.to validate_presence_of(:issues_url) } + it { is_expected.to validate_presence_of(:new_issue_url) } + it_behaves_like 'issue tracker service URL attribute', :api_url + it_behaves_like 'issue tracker service URL attribute', :project_url + it_behaves_like 'issue tracker service URL attribute', :issues_url + it_behaves_like 'issue tracker service URL attribute', :new_issue_url + end + + context 'when service is inactive' do + before { subject.active = false } + + it { is_expected.not_to validate_presence_of(:api_url) } + it { is_expected.not_to validate_presence_of(:project_url) } + it { is_expected.not_to validate_presence_of(:issues_url) } + it { is_expected.not_to validate_presence_of(:new_issue_url) } + end + end + describe "Execute" do let(:user) { create(:user) } let(:project) { create(:project) } @@ -72,7 +96,7 @@ describe JiraService, models: true do context "when a password was previously set" do before do - @jira_service = JiraService.create( + @jira_service = JiraService.create!( project: create(:project), properties: { api_url: 'http://jira.example.com/rest/api/2', diff --git a/spec/models/project_services/pivotaltracker_service_spec.rb b/spec/models/project_services/pivotaltracker_service_spec.rb new file mode 100644 index 00000000000..f37edd4d970 --- /dev/null +++ b/spec/models/project_services/pivotaltracker_service_spec.rb @@ -0,0 +1,42 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'spec_helper' + +describe PivotaltrackerService, models: true do + describe 'Associations' do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe 'Validations' do + context 'when service is active' do + before { subject.active = true } + + it { is_expected.to validate_presence_of(:token) } + end + + context 'when service is inactive' do + before { subject.active = false } + + it { is_expected.not_to validate_presence_of(:token) } + end + end +end diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb index 96039f9491b..555d9757b47 100644 --- a/spec/models/project_services/pushover_service_spec.rb +++ b/spec/models/project_services/pushover_service_spec.rb @@ -27,14 +27,20 @@ describe PushoverService, models: true do end describe 'Validations' do - context 'active' do - before do - subject.active = true - end + context 'when service is active' do + before { subject.active = true } - it { is_expected.to validate_presence_of :api_key } - it { is_expected.to validate_presence_of :user_key } - it { is_expected.to validate_presence_of :priority } + it { is_expected.to validate_presence_of(:api_key) } + it { is_expected.to validate_presence_of(:user_key) } + it { is_expected.to validate_presence_of(:priority) } + end + + context 'when service is inactive' do + before { subject.active = false } + + it { is_expected.not_to validate_presence_of(:api_key) } + it { is_expected.not_to validate_presence_of(:user_key) } + it { is_expected.not_to validate_presence_of(:priority) } end end diff --git a/spec/models/project_services/redmine_service_spec.rb b/spec/models/project_services/redmine_service_spec.rb new file mode 100644 index 00000000000..7d14f6e8280 --- /dev/null +++ b/spec/models/project_services/redmine_service_spec.rb @@ -0,0 +1,49 @@ +# == Schema Information +# +# Table name: services +# +# id :integer not null, primary key +# type :string(255) +# title :string(255) +# project_id :integer +# created_at :datetime +# updated_at :datetime +# active :boolean default(FALSE), not null +# properties :text +# template :boolean default(FALSE) +# push_events :boolean default(TRUE) +# issues_events :boolean default(TRUE) +# merge_requests_events :boolean default(TRUE) +# tag_push_events :boolean default(TRUE) +# note_events :boolean default(TRUE), not null +# + +require 'spec_helper' + +describe RedmineService, models: true do + describe 'Associations' do + it { is_expected.to belong_to :project } + it { is_expected.to have_one :service_hook } + end + + describe 'Validations' do + context 'when service is active' do + before { subject.active = true } + + it { is_expected.to validate_presence_of(:project_url) } + it { is_expected.to validate_presence_of(:issues_url) } + it { is_expected.to validate_presence_of(:new_issue_url) } + it_behaves_like 'issue tracker service URL attribute', :project_url + it_behaves_like 'issue tracker service URL attribute', :issues_url + it_behaves_like 'issue tracker service URL attribute', :new_issue_url + end + + context 'when service is inactive' do + before { subject.active = false } + + it { is_expected.not_to validate_presence_of(:project_url) } + it { is_expected.not_to validate_presence_of(:issues_url) } + it { is_expected.not_to validate_presence_of(:new_issue_url) } + end + end +end diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb index 478d59be08b..a97b7560137 100644 --- a/spec/models/project_services/slack_service_spec.rb +++ b/spec/models/project_services/slack_service_spec.rb @@ -26,13 +26,18 @@ describe SlackService, models: true do it { is_expected.to have_one :service_hook } end - describe "Validations" do - context "active" do - before do - subject.active = true - end + describe 'Validations' do + context 'when service is active' do + before { subject.active = true } - it { is_expected.to validate_presence_of :webhook } + it { is_expected.to validate_presence_of(:webhook) } + it_behaves_like 'issue tracker service URL attribute', :webhook + end + + context 'when service is inactive' do + before { subject.active = false } + + it { is_expected.not_to validate_presence_of(:webhook) } end end diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb index bc7423cee69..ad24b895170 100644 --- a/spec/models/project_services/teamcity_service_spec.rb +++ b/spec/models/project_services/teamcity_service_spec.rb @@ -27,86 +27,51 @@ describe TeamcityService, models: true do end describe 'Validations' do - describe '#teamcity_url' do - it 'does not validate the presence of teamcity_url if service is not active' do - teamcity_service = service - teamcity_service.active = false - - expect(teamcity_service).not_to validate_presence_of(:teamcity_url) - end - - it 'validates the presence of teamcity_url if service is active' do - teamcity_service = service - teamcity_service.active = true - - expect(teamcity_service).to validate_presence_of(:teamcity_url) - end - end + subject { service } - describe '#build_type' do - it 'does not validate the presence of build_type if service is not active' do - teamcity_service = service - teamcity_service.active = false + context 'when service is active' do + before { subject.active = true } - expect(teamcity_service).not_to validate_presence_of(:build_type) - end + it { is_expected.to validate_presence_of(:build_type) } + it { is_expected.to validate_presence_of(:teamcity_url) } + it_behaves_like 'issue tracker service URL attribute', :teamcity_url - it 'validates the presence of build_type if service is active' do - teamcity_service = service - teamcity_service.active = true + describe '#username' do + it 'does not validate the presence of username if password is nil' do + subject.password = nil - expect(teamcity_service).to validate_presence_of(:build_type) - end - end + expect(subject).not_to validate_presence_of(:username) + end - describe '#username' do - it 'does not validate the presence of username if service is not active' do - teamcity_service = service - teamcity_service.active = false + it 'validates the presence of username if password is present' do + subject.password = 'secret' - expect(teamcity_service).not_to validate_presence_of(:username) + expect(subject).to validate_presence_of(:username) + end end - it 'does not validate the presence of username if username is nil' do - teamcity_service = service - teamcity_service.active = true - teamcity_service.password = nil + describe '#password' do + it 'does not validate the presence of password if username is nil' do + subject.username = nil - expect(teamcity_service).not_to validate_presence_of(:username) - end + expect(subject).not_to validate_presence_of(:password) + end - it 'validates the presence of username if service is active and username is present' do - teamcity_service = service - teamcity_service.active = true - teamcity_service.password = 'secret' + it 'validates the presence of password if username is present' do + subject.username = 'john' - expect(teamcity_service).to validate_presence_of(:username) + expect(subject).to validate_presence_of(:password) + end end end - describe '#password' do - it 'does not validate the presence of password if service is not active' do - teamcity_service = service - teamcity_service.active = false - - expect(teamcity_service).not_to validate_presence_of(:password) - end - - it 'does not validate the presence of password if username is nil' do - teamcity_service = service - teamcity_service.active = true - teamcity_service.username = nil - - expect(teamcity_service).not_to validate_presence_of(:password) - end - - it 'validates the presence of password if service is active and username is present' do - teamcity_service = service - teamcity_service.active = true - teamcity_service.username = 'john' + context 'when service is inactive' do + before { subject.active = false } - expect(teamcity_service).to validate_presence_of(:password) - end + it { is_expected.not_to validate_presence_of(:build_type) } + it { is_expected.not_to validate_presence_of(:teamcity_url) } + it { is_expected.not_to validate_presence_of(:username) } + it { is_expected.not_to validate_presence_of(:password) } end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index e33c7d62ff4..5b1cf71337e 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -798,4 +798,18 @@ describe Project, models: true do end end end + + describe '#protected_branch?' do + let(:project) { create(:empty_project) } + + it 'returns true when a branch is a protected branch' do + project.protected_branches.create!(name: 'foo') + + expect(project.protected_branch?('foo')).to eq(true) + end + + it 'returns false when a branch is not a protected branch' do + expect(project.protected_branch?('foo')).to eq(false) + end + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index c19524a01f8..397bb5a8028 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -134,7 +134,43 @@ describe Repository, models: true do end end - describe '#license_blob' do + describe "#changelog" do + before do + repository.send(:cache).expire(:changelog) + end + + it 'accepts changelog' do + expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changelog')]) + + expect(repository.changelog.name).to eq('changelog') + end + + it 'accepts news instead of changelog' do + expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('news')]) + + expect(repository.changelog.name).to eq('news') + end + + it 'accepts history instead of changelog' do + expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('history')]) + + expect(repository.changelog.name).to eq('history') + end + + it 'accepts changes instead of changelog' do + expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changes')]) + + expect(repository.changelog.name).to eq('changes') + end + + it 'is case-insensitive' do + expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('CHANGELOG')]) + + expect(repository.changelog.name).to eq('CHANGELOG') + end + end + + describe "#license_blob" do before do repository.send(:cache).expire(:license_blob) repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master') @@ -759,6 +795,16 @@ describe Repository, models: true do end + describe "#copy_gitattributes" do + it 'returns true with a valid ref' do + expect(repository.copy_gitattributes('master')).to be_truthy + end + + it 'returns false with an invalid ref' do + expect(repository.copy_gitattributes('invalid')).to be_falsey + end + end + describe "#main_language" do it 'shows the main language of the project' do expect(repository.main_language).to eq("Ruby") diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index 344f0fe0b7f..241995041bb 100644 --- a/spec/requests/api/milestones_spec.rb +++ b/spec/requests/api/milestones_spec.rb @@ -127,7 +127,7 @@ describe API::API, api: true do describe 'GET /projects/:id/milestones/:milestone_id/issues' do before do - milestone.issues << create(:issue) + milestone.issues << create(:issue, project: project) end it 'should return project issues for a particular milestone' do get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user) @@ -140,5 +140,34 @@ describe API::API, api: true do get api("/projects/#{project.id}/milestones/#{milestone.id}/issues") expect(response.status).to eq(401) end + + describe 'confidential issues' do + let(:public_project) { create(:project, :public) } + let(:milestone) { create(:milestone, project: public_project) } + let(:issue) { create(:issue, project: public_project) } + let(:confidential_issue) { create(:issue, confidential: true, project: public_project) } + before do + public_project.team << [user, :developer] + milestone.issues << issue << confidential_issue + end + + it 'returns confidential issues to team members' do + get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(2) + expect(json_response.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id) + end + + it 'does not return confidential issues to regular users' do + get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", create(:user)) + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.map { |issue| issue['id'] }).to include(issue.id) + end + end end end diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index ec9eda0a2ed..49091fc0f49 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } - let!(:project) { create(:project, namespace: user.namespace ) } + let!(:project) { create(:project, namespace: user.namespace) } let!(:issue) { create(:issue, project: project, author: user) } let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) } let!(:snippet) { create(:project_snippet, project: project, author: user) } @@ -45,7 +45,7 @@ describe API::API, api: true do end it "should return a 404 error when issue id not found" do - get api("/projects/#{project.id}/issues/123/notes", user) + get api("/projects/#{project.id}/issues/12345/notes", user) expect(response.status).to eq(404) end @@ -106,7 +106,7 @@ describe API::API, api: true do end it "should return a 404 error if issue note not found" do - get api("/projects/#{project.id}/issues/#{issue.id}/notes/123", user) + get api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user) expect(response.status).to eq(404) end @@ -134,7 +134,7 @@ describe API::API, api: true do end it "should return a 404 error if snippet note not found" do - get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/123", user) + get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/12345", user) expect(response.status).to eq(404) end end @@ -191,6 +191,27 @@ describe API::API, api: true do expect(response.status).to eq(401) end end + + context 'when user does not have access to create noteable' do + let(:private_issue) { create(:issue, project: create(:project, :private)) } + + ## + # We are posting to project user has access to, but we use issue id + # from a different project, see #15577 + # + before do + post api("/projects/#{project.id}/issues/#{private_issue.id}/notes", user), + body: 'Hi!' + end + + it 'responds with 500' do + expect(response.status).to eq 500 + end + + it 'does not create new note' do + expect(private_issue.notes.reload).to be_empty + end + end end describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do @@ -211,7 +232,7 @@ describe API::API, api: true do end it 'should return a 404 error when note id not found' do - put api("/projects/#{project.id}/issues/#{issue.id}/notes/123", user), + put api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user), body: 'Hello!' expect(response.status).to eq(404) end @@ -233,7 +254,7 @@ describe API::API, api: true do it 'should return a 404 error when note id not found' do put api("/projects/#{project.id}/snippets/#{snippet.id}/"\ - "notes/123", user), body: "Hello!" + "notes/12345", user), body: "Hello!" expect(response.status).to eq(404) end end @@ -248,7 +269,7 @@ describe API::API, api: true do it 'should return a 404 error when note id not found' do put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\ - "notes/123", user), body: "Hello!" + "notes/12345", user), body: "Hello!" expect(response.status).to eq(404) end end @@ -268,7 +289,7 @@ describe API::API, api: true do end it 'returns a 404 error when note id not found' do - delete api("/projects/#{project.id}/issues/#{issue.id}/notes/123", user) + delete api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user) expect(response.status).to eq(404) end @@ -288,7 +309,7 @@ describe API::API, api: true do it 'returns a 404 error when note id not found' do delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\ - "notes/123", user) + "notes/12345", user) expect(response.status).to eq(404) end @@ -308,7 +329,7 @@ describe API::API, api: true do it 'returns a 404 error when note id not found' do delete api("/projects/#{project.id}/merge_requests/"\ - "#{merge_request.id}/notes/123", user) + "#{merge_request.id}/notes/12345", user) expect(response.status).to eq(404) end diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 3722ddf5a33..9706d060cfa 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -15,4 +15,91 @@ describe API::API, api: true do expect(json_response['expires_at']).to be_nil end end + + describe 'GET /projects/:project_id/snippets/' do + it 'all snippets available to team member' do + project = create(:project, :public) + user = create(:user) + project.team << [user, :developer] + public_snippet = create(:project_snippet, :public, project: project) + internal_snippet = create(:project_snippet, :internal, project: project) + private_snippet = create(:project_snippet, :private, project: project) + + get api("/projects/#{project.id}/snippets/", user) + + expect(response.status).to eq(200) + expect(json_response.size).to eq(3) + expect(json_response.map{ |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id) + end + + it 'hides private snippets from regular user' do + project = create(:project, :public) + user = create(:user) + create(:project_snippet, :private, project: project) + + get api("/projects/#{project.id}/snippets/", user) + expect(response.status).to eq(200) + expect(json_response.size).to eq(0) + end + end + + describe 'POST /projects/:project_id/snippets/' do + it 'creates a new snippet' do + admin = create(:admin) + project = create(:project) + params = { + title: 'Test Title', + file_name: 'test.rb', + code: 'puts "hello world"', + visibility_level: Gitlab::VisibilityLevel::PUBLIC + } + + post api("/projects/#{project.id}/snippets/", admin), params + + expect(response.status).to eq(201) + snippet = ProjectSnippet.find(json_response['id']) + expect(snippet.content).to eq(params[:code]) + expect(snippet.title).to eq(params[:title]) + expect(snippet.file_name).to eq(params[:file_name]) + expect(snippet.visibility_level).to eq(params[:visibility_level]) + end + end + + describe 'PUT /projects/:project_id/snippets/:id/' do + it 'updates snippet' do + admin = create(:admin) + snippet = create(:project_snippet, author: admin) + new_content = 'New content' + + put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), code: new_content + + expect(response.status).to eq(200) + snippet.reload + expect(snippet.content).to eq(new_content) + end + end + + describe 'DELETE /projects/:project_id/snippets/:id/' do + it 'deletes snippet' do + admin = create(:admin) + snippet = create(:project_snippet, author: admin) + + delete api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin) + + expect(response.status).to eq(200) + end + end + + describe 'GET /projects/:project_id/snippets/:id/raw' do + it 'returns raw text' do + admin = create(:admin) + snippet = create(:project_snippet, author: admin) + + get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/raw", admin) + + expect(response.status).to eq(200) + expect(response.content_type).to eq 'text/plain' + expect(response.body).to eq(snippet.content) + end + end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index fccd08bd6da..66193eac051 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -11,7 +11,7 @@ describe API::API, api: true do let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } let(:project2) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace) } let(:project3) { create(:project, path: 'project3', creator_id: user.id, namespace: user.namespace) } - let(:snippet) { create(:project_snippet, author: user, project: project, title: 'example') } + let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') } let(:project_member) { create(:project_member, :master, user: user, project: project) } let(:project_member2) { create(:project_member, :developer, user: user3, project: project) } let(:user4) { create(:user) } diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index b40a5c1c818..eeab540c2fd 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -201,6 +201,36 @@ describe GitPushService, services: true do end + describe "Updates git attributes" do + context "for default branch" do + it "calls the copy attributes method for the first push to the default branch" do + expect(project.repository).to receive(:copy_gitattributes).with('master') + + execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master') + end + + it "calls the copy attributes method for changes to the default branch" do + expect(project.repository).to receive(:copy_gitattributes).with('refs/heads/master') + + execute_service(project, user, 'oldrev', 'newrev', 'refs/heads/master') + end + end + + context "for non-default branch" do + before do + # Make sure the "default" branch is different + allow(project).to receive(:default_branch).and_return('not-master') + end + + it "does not call copy attributes method" do + expect(project.repository).not_to receive(:copy_gitattributes) + + execute_service(project, user, @oldrev, @newrev, @ref) + end + end + end + + describe "Webhooks" do context "execute webhooks" do it "when pushing a branch for the first time" do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index d7c72dc0811..4bbc4ddc3ab 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -10,7 +10,7 @@ describe NotificationService, services: true do end describe 'Keys' do - describe :new_key do + describe '#new_key' do let!(:key) { create(:personal_key) } it { expect(notification.new_key(key)).to be_truthy } @@ -22,7 +22,7 @@ describe NotificationService, services: true do end describe 'Email' do - describe :new_email do + describe '#new_email' do let!(:email) { create(:email) } it { expect(notification.new_email(email)).to be_truthy } @@ -147,8 +147,8 @@ describe NotificationService, services: true do ActionMailer::Base.deliveries.clear end - describe :new_note do - it do + describe '#new_note' do + it 'notifies the team members' do notification.new_note(note) # Notify all team members @@ -177,6 +177,39 @@ describe NotificationService, services: true do end end + context 'project snippet note' do + let(:project) { create(:empty_project, :public) } + let(:snippet) { create(:project_snippet, project: project, author: create(:user)) } + let(:note) { create(:note_on_project_snippet, noteable: snippet, project_id: snippet.project.id, note: '@all mentioned') } + + before do + build_team(note.project) + note.project.team << [note.author, :master] + ActionMailer::Base.deliveries.clear + end + + describe '#new_note' do + it 'notifies the team members' do + notification.new_note(note) + + # Notify all team members + note.project.team.members.each do |member| + # User with disabled notification should not be notified + next if member.id == @u_disabled.id + # Author should not be notified + next if member.id == note.author.id + should_email(member) + end + + should_email(note.noteable.author) + should_not_email(note.author) + should_email(@u_mentioned) + should_not_email(@u_disabled) + should_email(@u_not_mentioned) + end + end + end + context 'commit note' do let(:project) { create(:project, :public) } let(:note) { create(:note_on_commit, project: project) } @@ -187,7 +220,7 @@ describe NotificationService, services: true do allow_any_instance_of(Commit).to receive(:author).and_return(@u_committer) end - describe :new_note, :perform_enqueued_jobs do + describe '#new_note, #perform_enqueued_jobs' do it do notification.new_note(note) @@ -230,7 +263,7 @@ describe NotificationService, services: true do ActionMailer::Base.deliveries.clear end - describe :new_issue do + describe '#new_issue' do it do notification.new_issue(issue, @u_disabled) @@ -289,7 +322,7 @@ describe NotificationService, services: true do end end - describe :reassigned_issue do + describe '#reassigned_issue' do it 'emails new assignee' do notification.reassigned_issue(issue, @u_disabled) @@ -419,7 +452,7 @@ describe NotificationService, services: true do end end - describe :close_issue do + describe '#close_issue' do it 'should sent email to issue assignee and issue author' do notification.close_issue(issue, @u_disabled) @@ -435,7 +468,7 @@ describe NotificationService, services: true do end end - describe :reopen_issue do + describe '#reopen_issue' do it 'should send email to issue assignee and issue author' do notification.reopen_issue(issue, @u_disabled) @@ -461,7 +494,7 @@ describe NotificationService, services: true do ActionMailer::Base.deliveries.clear end - describe :new_merge_request do + describe '#new_merge_request' do it do notification.new_merge_request(merge_request, @u_disabled) @@ -483,7 +516,7 @@ describe NotificationService, services: true do end end - describe :reassigned_merge_request do + describe '#reassigned_merge_request' do it do notification.reassigned_merge_request(merge_request, merge_request.author) @@ -498,7 +531,7 @@ describe NotificationService, services: true do end end - describe :relabel_merge_request do + describe '#relabel_merge_request' do let(:label) { create(:label, merge_requests: [merge_request]) } let(:label2) { create(:label) } let!(:subscriber_to_label) { create(:user).tap { |u| label.toggle_subscription(u) } } @@ -527,7 +560,7 @@ describe NotificationService, services: true do end end - describe :closed_merge_request do + describe '#closed_merge_request' do it do notification.close_mr(merge_request, @u_disabled) @@ -542,7 +575,7 @@ describe NotificationService, services: true do end end - describe :merged_merge_request do + describe '#merged_merge_request' do it do notification.merge_mr(merge_request, @u_disabled) @@ -557,7 +590,7 @@ describe NotificationService, services: true do end end - describe :reopen_merge_request do + describe '#reopen_merge_request' do it do notification.reopen_mr(merge_request, @u_disabled) @@ -581,7 +614,7 @@ describe NotificationService, services: true do ActionMailer::Base.deliveries.clear end - describe :project_was_moved do + describe '#project_was_moved' do it do notification.project_was_moved(project, "gitlab/gitlab") diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index 32bf3acf483..7f2dcdab960 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -112,9 +112,16 @@ describe Projects::ImportService, services: true do def stub_github_omniauth_provider provider = OpenStruct.new( - name: 'github', - app_id: 'asd123', - app_secret: 'asd123' + 'name' => 'github', + 'app_id' => 'asd123', + 'app_secret' => 'asd123', + 'args' => { + 'client_options' => { + 'site' => 'https://github.com/api/v3', + 'authorize_url' => 'https://github.com/login/oauth/authorize', + 'token_url' => 'https://github.com/login/oauth/access_token' + } + } ) Gitlab.config.omniauth.providers << provider diff --git a/spec/support/issue_tracker_service_shared_example.rb b/spec/support/issue_tracker_service_shared_example.rb new file mode 100644 index 00000000000..b6d7436c360 --- /dev/null +++ b/spec/support/issue_tracker_service_shared_example.rb @@ -0,0 +1,7 @@ +RSpec.shared_examples 'issue tracker service URL attribute' do |url_attr| + it { is_expected.to allow_value('https://example.com').for(url_attr) } + + it { is_expected.not_to allow_value('example.com').for(url_attr) } + it { is_expected.not_to allow_value('ftp://example.com').for(url_attr) } + it { is_expected.not_to allow_value('herp-and-derp').for(url_attr) } +end diff --git a/spec/workers/repository_check/batch_worker_spec.rb b/spec/workers/repository_check/batch_worker_spec.rb index f486e45ddad..27727d6abf9 100644 --- a/spec/workers/repository_check/batch_worker_spec.rb +++ b/spec/workers/repository_check/batch_worker_spec.rb @@ -4,7 +4,7 @@ describe RepositoryCheck::BatchWorker do subject { described_class.new } it 'prefers projects that have never been checked' do - projects = create_list(:project, 3) + projects = create_list(:project, 3, created_at: 1.week.ago) projects[0].update_column(:last_repository_check_at, 4.months.ago) projects[2].update_column(:last_repository_check_at, 3.months.ago) @@ -12,7 +12,7 @@ describe RepositoryCheck::BatchWorker do end it 'sorts projects by last_repository_check_at' do - projects = create_list(:project, 3) + projects = create_list(:project, 3, created_at: 1.week.ago) projects[0].update_column(:last_repository_check_at, 2.months.ago) projects[1].update_column(:last_repository_check_at, 4.months.ago) projects[2].update_column(:last_repository_check_at, 3.months.ago) @@ -21,7 +21,7 @@ describe RepositoryCheck::BatchWorker do end it 'excludes projects that were checked recently' do - projects = create_list(:project, 3) + projects = create_list(:project, 3, created_at: 1.week.ago) projects[0].update_column(:last_repository_check_at, 2.days.ago) projects[1].update_column(:last_repository_check_at, 2.months.ago) projects[2].update_column(:last_repository_check_at, 3.days.ago) @@ -30,10 +30,17 @@ describe RepositoryCheck::BatchWorker do end it 'does nothing when repository checks are disabled' do - create(:empty_project) + create(:empty_project, created_at: 1.week.ago) current_settings = double('settings', repository_checks_enabled: false) expect(subject).to receive(:current_settings) { current_settings } expect(subject.perform).to eq(nil) end + + it 'skips projects created less than 24 hours ago' do + project = create(:empty_project) + project.update_column(:created_at, 23.hours.ago) + + expect(subject.perform).to eq([]) + end end |