diff --git a/.rubocop.yml b/.rubocop.yml index 83ed6c3867825f1e8853335e396d0eeb17c5795e..9f179efa3ce0ba9a2f4a4ffa097e31f6ee34e737 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -953,10 +953,9 @@ Performance/DoubleStartEndWith: Performance/EndWith: Enabled: false -# TODO: Enable LstripRstrip Cop. # Use `strip` instead of `lstrip.rstrip`. Performance/LstripRstrip: - Enabled: false + Enabled: true # TODO: Enable RangeInclude Cop. # Use `Range#cover?` instead of `Range#include?`. diff --git a/CHANGELOG b/CHANGELOG index ed151df9e25349188d1f0f8cc6f6e42899b82ade..fed3caef7e8d7ebf1f9504e75ae94957100b823a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,22 +1,48 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.8.0 (unreleased) + - Assign labels and milestone to target project when moving issue. !3934 (Long Nguyen) + - Project#open_branches has been cleaned up and no longer loads entire records into memory. + - Log to application.log when an admin starts and stops impersonating a user + - Updated gitlab_git to 10.1.0 + - GitAccess#protected_tag? no longer loads all tags just to check if a single one exists + - Reduce delay in destroying a project from 1-minute to immediately - Make build status canceled if any of the jobs was canceled and none failed + - Upgrade Sidekiq to 4.1.2 + - Sanitize repo paths in new project error message + - Bump mail_room to 0.7.0 to fix stuck IDLE connections - Remove future dates from contribution calendar graph. + - 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 + - Update SVG sanitizer to conform to SVG 1.1 - 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 + - Backport GitHub Enterprise import support from EE + - Create tags using Rugged for performance reasons. !3745 - 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. + - Improve description for the Two-factor Authentication sign-in screen. (Connor Shea) + - API support for the 'since' and 'until' operators on commit requests (Paco Guzman) + - Fix Gravatar hint in user profile when Gravatar is disabled. !3988 (Artem Sidorenko) + - Expire repository exists? and has_visible_content? caches after a push if necessary + - Fix unintentional filtering bug in issues sorted by milestone due (Takuya Noguchi) + +v 8.7.3 + - Emails, Gitlab::Email::Message, Gitlab::Diff, and Premailer::Adapter::Nokogiri are now instrumented + - Merge request widget displays TeamCity build state and code coverage correctly again. + - Fix the line code when importing PR review comments from GitHub. !4010 + - Wikis are now initialized on legacy projects when checking repositories v 8.7.2 - The "New Branch" button is now loaded asynchronously + - Fix error 500 when trying to create a wiki page + - Updated spacing between notification label and button + - Label titles in filters are now escaped properly v 8.7.1 - Throttle the update of `project.last_activity_at` to 1 minute. !3848 @@ -138,13 +164,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 @@ -286,6 +324,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 @@ -436,6 +485,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 @@ -561,6 +621,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 @@ -670,6 +739,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 084c2d616a986fa20547dcaa261c382ced24d6af..9fe4cf7b0f6d8ffc4ae9e2e5923daf8ceb32d7b1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -142,6 +142,16 @@ 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. +You are encouraged to use the template below for feature proposals. + +``` +## Description including problem, use cases, benefits, and/or goals + +## Proposal + +## Links / references +``` + For changes in the interface, it can be helpful to create a mockup first. If you want to create something yourself, consider opening an issue first to discuss whether it is interesting to include this in GitLab. @@ -349,7 +359,7 @@ on your merge request feel free to mention one of the Merge Marshalls in the Please ensure that your merge request meets the contribution acceptance criteria. When having your code reviewed and when reviewing merge requests please take the -[Thoughtbot code review guide] into account. +[code review guidelines](doc/development/code_review.md) into account. ### Merge request description format @@ -523,4 +533,3 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design [free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12 [`gitlab1.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/gitlab1.atype/ -[Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review diff --git a/Gemfile b/Gemfile index 0301f6fe06222793b373b24363ccbc9bdf4d8cf7..c7db12a19477633020d501321691e82d71287f72 100644 --- a/Gemfile +++ b/Gemfile @@ -19,7 +19,8 @@ gem "pg", '~> 0.18.2', group: :postgres # Authentication libraries gem 'devise', '~> 3.5.4' -gem 'doorkeeper', '~> 2.2.0' +gem 'doorkeeper', '~> 3.1' +gem 'devise-async', '~> 0.9.0' gem 'omniauth', '~> 1.3.1' gem 'omniauth-auth0', '~> 1.4.1' gem 'omniauth-azure-oauth2', '~> 0.0.6' @@ -271,7 +272,7 @@ group :development, :test do gem 'database_cleaner', '~> 1.4.0' gem 'factory_girl_rails', '~> 4.6.0' - gem 'rspec-rails', '~> 3.3.0' + gem 'rspec-rails', '~> 3.4.0' gem 'rspec-retry' gem 'spinach-rails', '~> 0.2.1' gem 'spinach-rerun-reporter', '~> 0.0.2' @@ -321,7 +322,7 @@ gem "newrelic_rpm", '~> 3.14' gem 'octokit', '~> 4.3.0' -gem "mail_room", "~> 0.6.1" +gem "mail_room", "~> 0.7" gem 'email_reply_parser', '~> 0.5.8' diff --git a/Gemfile.lock b/Gemfile.lock index 2b1cfdc9bb2a4a5ca680ea77b76671818b747714..36efb66665ff2124d3c293ec1c48bdab8e374a20 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -135,7 +135,7 @@ GEM execjs coffee-script-source (1.10.0) colorize (0.7.7) - concurrent-ruby (1.0.1) + concurrent-ruby (1.0.2) connection_pool (2.2.0) coveralls (0.8.13) json (~> 1.8) @@ -165,6 +165,8 @@ 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) @@ -174,7 +176,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) @@ -352,7 +354,7 @@ GEM posix-spawn (~> 0.3) gitlab_emoji (0.3.1) gemojione (~> 2.2, >= 2.2.1) - gitlab_git (10.0.1) + gitlab_git (10.1.0) activesupport (~> 4.0) charlock_holmes (~> 0.7.3) github-linguist (~> 4.7.0) @@ -466,7 +468,7 @@ GEM systemu (~> 2.6.2) mail (2.6.4) mime-types (>= 1.16, < 4) - mail_room (0.6.1) + mail_room (0.7.0) method_source (0.8.2) mime-types (2.99.1) mimemagic (0.3.0) @@ -663,29 +665,29 @@ GEM chunky_png rqrcode-rails3 (0.1.7) rqrcode (>= 0.4.2) - rspec (3.3.0) - rspec-core (~> 3.3.0) - rspec-expectations (~> 3.3.0) - rspec-mocks (~> 3.3.0) - rspec-core (3.3.2) - rspec-support (~> 3.3.0) - rspec-expectations (3.3.1) + rspec (3.4.0) + rspec-core (~> 3.4.0) + rspec-expectations (~> 3.4.0) + rspec-mocks (~> 3.4.0) + rspec-core (3.4.4) + rspec-support (~> 3.4.0) + rspec-expectations (3.4.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.3.0) - rspec-mocks (3.3.2) + rspec-support (~> 3.4.0) + rspec-mocks (3.4.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.3.0) - rspec-rails (3.3.3) + rspec-support (~> 3.4.0) + rspec-rails (3.4.2) actionpack (>= 3.0, < 4.3) activesupport (>= 3.0, < 4.3) railties (>= 3.0, < 4.3) - rspec-core (~> 3.3.0) - rspec-expectations (~> 3.3.0) - rspec-mocks (~> 3.3.0) - rspec-support (~> 3.3.0) + rspec-core (~> 3.4.0) + rspec-expectations (~> 3.4.0) + rspec-mocks (~> 3.4.0) + rspec-support (~> 3.4.0) rspec-retry (0.4.5) rspec-core - rspec-support (3.3.0) + rspec-support (3.4.1) rubocop (0.38.0) parser (>= 2.3.0.6, < 3.0) powerpack (~> 0.1) @@ -739,7 +741,7 @@ GEM rack shoulda-matchers (2.8.0) activesupport (>= 3.0.0) - sidekiq (4.1.1) + sidekiq (4.1.2) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) redis (~> 3.2, >= 3.2.1) @@ -922,9 +924,10 @@ 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) @@ -964,7 +967,7 @@ DEPENDENCIES letter_opener_web (~> 1.3.0) licensee (~> 8.0.0) loofah (~> 2.0.3) - mail_room (~> 0.6.1) + mail_room (~> 0.7) method_source (~> 0.8) minitest (~> 5.7.0) mousetrap-rails (~> 1.4.6) @@ -1014,7 +1017,7 @@ DEPENDENCIES responders (~> 2.0) rouge (~> 1.10.1) rqrcode-rails3 (~> 0.1.7) - rspec-rails (~> 3.3.0) + rspec-rails (~> 3.4.0) rspec-retry rubocop (~> 0.38.0) ruby-fogbugz (~> 0.2.1) @@ -1061,4 +1064,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.11.2 + 1.12.1 diff --git a/README.md b/README.md index afa60116ebba5b1c38e2e4b9174d90d41f6c74e3..5e41665a6ba8f6159bd57bbaefb16571d6c6cc3f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # GitLab -[](https://ci.gitlab.com/projects/1?ref=master) +[](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) [](https://semaphoreci.com/gitlabhq/gitlabhq) [](https://codeclimate.com/github/gitlabhq/gitlabhq) [](https://coveralls.io/r/gitlabhq/gitlabhq?branch=master) @@ -20,6 +20,10 @@ To see how GitLab looks please see the [features page on our website](https://ab - Completely free and open source (MIT Expat license) - Powered by [Ruby on Rails](https://github.com/rails/rails) +## Hiring + +We're hiring developers, support people, and production engineers all the time, please see our [jobs page](https://about.gitlab.com/jobs/). + ## Editions There are two editions of GitLab: @@ -80,7 +84,7 @@ There are a lot of [third-party applications integrating with GitLab](https://ab ## GitLab release cycle -For more information about the release process see the [release documentation](http://doc.gitlab.com/ce/release/). +For more information about the release process see the [release documentation](https://gitlab.com/gitlab-org/release-tools/blob/master/README.md). ## Upgrading diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee index fcba9818726a3a79df06be02892cbb8d6f77dd87..bf95e06b4e5e8336168402982c0a3bbc3b4aefbe 100644 --- a/app/assets/javascripts/awards_handler.coffee +++ b/app/assets/javascripts/awards_handler.coffee @@ -1,58 +1,58 @@ class @AwardsHandler - constructor: (@get_emojis_url, @post_emoji_url, @noteable_type, @noteable_id, @unicodes) -> - $(".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 @@ -60,7 +60,7 @@ class @AwardsHandler @postEmoji emoji, => @addAwardToEmojiBar(emoji) - $(".emoji-menu").removeClass "is-visible" + $('.emoji-menu').removeClass 'is-visible' addAwardToEmojiBar: (emoji) -> @addEmojiToFrequentlyUsedList(emoji) @@ -69,9 +69,9 @@ class @AwardsHandler 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,20 +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-#{@unicodes[emoji]}" + emojiIcon = $(".emoji-menu-content [data-emoji='#{emoji}']") + + if emojiIcon.length > 0 + unicodeName = emojiIcon.data('unicode-name') + else + # Find by alias + 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() @@ -166,42 +174,42 @@ class @AwardsHandler }, 200) 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/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee index 7d61cd9bf73ffee075db5d7bb56a0f8a795757c0..995fd768603f92bc64f035cbb999e2f6c0c0f5d6 100644 --- a/app/assets/javascripts/labels_select.js.coffee +++ b/app/assets/javascripts/labels_select.js.coffee @@ -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/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee index 065626beeb8bfb94d030af45cf1cb7fe930a5ccc..17a5a057a944e07f548fec6055de3591c880f018 100644 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -68,20 +68,18 @@ class @MergeRequestWidget $.getJSON @opts.ci_status_url, (data) => @readyForCICheck = true - if @firstCICheck - @firstCICheck = false - @opts.ci_status = data.status - - if @opts.ci_status is '' - @opts.ci_status = data.status + if data.status is '' return - if data.status isnt @opts.ci_status and data.status? + if @firstCiCheck || data.status isnt @opts.ci_status and data.status? + @opts.ci_status = data.status @showCIStatus data.status if data.coverage @showCICoverage data.coverage - if showNotification + # The first check should only update the UI, a notification + # should only be displayed on status changes + if showNotification and not @firstCiCheck status = @ciLabelForStatus(data.status) if status is "preparing" @@ -104,8 +102,7 @@ class @MergeRequestWidget @close() Turbolinks.visit _this.opts.builds_path ) - - @opts.ci_status = data.status + @firstCiCheck = false showCIStatus: (state) -> $('.ci_widget').hide() diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index 82e210fed7d200a7ee39d1c4f25bef48ff5202db..efb3e8e219817a571610fe00b05e31d08b14f2b7 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/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index c85ab9148d034ab9e20794f5c36873258165c236..560de9fc0bddb60eade73fa8c8fbe7750083b8ad 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/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 5aa425dab6c9214e0d11254c8014c29e38fd6ee0..f5ce70b606b04767185baf093246078b62beca95 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -28,6 +28,7 @@ &.s46 { width: 46px; height: 46px; margin-right: 15px; } &.s48 { width: 48px; height: 48px; margin-right: 10px; } &.s60 { width: 60px; height: 60px; margin-right: 12px; } + &.s70 { width: 70px; height: 70px; margin-right: 14px; } &.s90 { width: 90px; height: 90px; margin-right: 15px; } &.s110 { width: 110px; height: 110px; margin-right: 15px; } &.s140 { width: 140px; height: 140px; margin-right: 20px; } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index e72e4aa47ef201b52090fb70f17eff1fd3aabf9e..434a26d57c66723c5991c3c918a73f0016b8fc34 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -18,7 +18,7 @@ line-height: 36px; } -.gray-content-block { +.row-content-block { margin-top: 0; margin-bottom: -$gl-padding; background-color: $background-color; @@ -81,6 +81,11 @@ margin-left: 10px; } } + + &.build-content { + background-color: $white-light; + border-top: none; + } } .cover-block { @@ -113,7 +118,7 @@ line-height: 1.1; h1 { - color: #313236; + color: $gl-gray-dark; margin-bottom: 6px; font-size: 23px; } @@ -150,6 +155,41 @@ right: auto; } } + + &.groups-cover-block { + background: $white-light; + border-bottom: 1px solid $border-color; + text-align: left; + padding: 24px 0; + + .group-info { + .cover-title { + margin-top: 9px; + } + + p { + margin-bottom: 0; + } + } + + @media (max-width: $screen-xs-max) { + text-align: center; + + .avatar { + float: none; + } + } + } + + .group-info { + + h1 { + display: inline; + font-weight: normal; + font-size: 24px; + color: $gl-title-color; + } + } } .block-connector { diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 062da397b6beab9a05276fc2c4c6ce226c02107d..eaf85bb17ca4876c191e853905d8e9756bf6c44e 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -59,7 +59,7 @@ } @mixin btn-gray { - @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-light, $gray-dark, $border-gray-dark, #313236); + @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-light, $gray-dark, $border-gray-dark, $gl-gray-dark); } @mixin btn-white { @@ -251,3 +251,10 @@ .btn-file-option { background: linear-gradient(180deg, $white-light 25%, $gray-light 100%); } + +.btn-build { + margin-left: 10px; + i { + color: $gl-icon-color; + } +} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 5fa10d29a87d76e4cdce10e44930ca695a38de09..97f9d5820073c7b92ef6dbb22dd30f4304ef3321 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -30,6 +30,10 @@ header { border: none; border-bottom: 1px solid $border-color; + &.with-horizontal-nav { + border-bottom: none; + } + .container-fluid { width: 100% !important; filter: none; diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 7eb451c124ef991a1f7c8332d313f924fe4230a2..33cbee85987ae7220ce0470e55d57a14d4efc089 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -30,7 +30,7 @@ } .rss-btn { - display: none !important; + display: none; } .project-home-links { diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss new file mode 100644 index 0000000000000000000000000000000000000000..26ad2870aa00834bf8cec81b70514f954b5cc3e6 --- /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 5fe687dcec35b8d6077e55ed282d97c8eb26c9b4..7c18e93a261c9fba650734e015b1f1149325f484 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -26,8 +26,8 @@ } &.active a { - color: #000; - border-bottom: 2px solid #4688f1; + border-bottom: 2px solid $link-underline-blue; + color: $black; } .badge { @@ -140,6 +140,12 @@ } } + .project-filter-form { + input { + background-color: $background-color; + } + } + @media (max-width: $screen-xs-max) { padding-bottom: 0; @@ -187,13 +193,31 @@ } .layout-nav { + position: fixed; + top: $header-height; + width: 100%; + z-index: 1; background: $background-color; border-bottom: 1px solid $border-color; + transition-duration: .3s; .controls { float: right; - position: relative; - top: 10px; + padding: 7px 5px 0 0; + + i { + color: $layout-link-gray; + } + + .fa-rss, + .fa-cog { + font-size: 16px; + } + + .fa-caret-down { + margin-left: 5px; + color: $gl-icon-color; + } .dropdown { margin-left: 7px; @@ -202,5 +226,34 @@ .nav-links { border-bottom: none; + height: 51px; + white-space: nowrap; + overflow-x: auto; + + li { + + a { + padding-top: 10px; + } + + a, i { + color: $layout-link-gray; + } + + &.active { + a, i { + color: $black; + } + } + + .badge { + color: $gl-icon-color; + } + } } + +} + +.page-with-layout-nav { + margin-top: 50px; } diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 18189e985c46f546dfadffd37b1d6ba3b098c0c5..e940fd7286eab605a3f1a5826515c8fb179ded6f 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -3,6 +3,7 @@ position: absolute; width: 58px; cursor: pointer; + margin-top: 8px; } .page-with-sidebar { @@ -62,7 +63,7 @@ float: left; height: $header-height; width: 100%; - padding: 11px 0 11px 22px; + padding-left: 22px; overflow: hidden; outline: none; transition-duration: .3s; @@ -85,7 +86,7 @@ margin: 0; margin-left: 50px; font-size: 19px; - line-height: 41px; + line-height: 50px; font-weight: normal; } } @@ -254,6 +255,10 @@ } } } + + .layout-nav { + padding-right: $sidebar_collapsed_width; + } } .page-sidebar-expanded { @@ -280,6 +285,10 @@ } } } + + .layout-nav { + padding-right: $sidebar_width; + } } .right-sidebar-collapsed { diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index 75b770ae5a275317887ed1f0cc53bf6af41abfd0..b42075c98d06f44ffb70d67eb94f1c51f8e7dcf1 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -32,13 +32,11 @@ table { th { background-color: $background-color; font-weight: normal; - font-size: 15px; - border-bottom: 1px solid $border-color; + border-bottom: none; } td { border-color: $table-border-color; - border-bottom: 1px solid $border-color; } } } diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 96bab7880c2156b2d21efa82c0993ff1f071579a..6a45c34ccbbeee78671b522ea66c7d1ae96ac6cc 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -81,7 +81,7 @@ // Labels .label { - padding: 2px 4px; + padding: 4px 5px; font-size: 13px; font-style: normal; font-weight: normal; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index b2535ddf4bd5c9f0a5ea994d33b327c01db36e17..2779cd56788bd069a2964d747cf3df3a2672ae7c 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -42,14 +42,14 @@ margin: 24px 0 12px; padding: 0 0 10px; border-bottom: 1px solid #e7e9ed; - color: #313236; + color: $gl-gray-dark; } h2 { font-size: 1.2em; font-weight: 600; margin: 24px 0 12px; - color: #313236; + color: $gl-gray-dark; } h3 { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index b8ed7e8a74ce21ba7ac464ed832157c2a8fe8ab2..ccb4e5381b7930816cc0ef2c3faa38df8ff2dbba 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -12,7 +12,7 @@ $gutter_inner_width: 258px; */ $border-color: #e5e5e5; $focus-border-color: #3aabf0; -$table-border-color: #eef0f2; +$table-border-color: #ececec; $background-color: #fafafa; /* @@ -20,7 +20,7 @@ $background-color: #fafafa; */ $gl-font-size: 15px; $gl-title-color: #333; -$gl-text-color: #555; +$gl-text-color: #5c5c5c; $gl-text-green: #4a2; $gl-text-red: #d12f19; $gl-text-orange: #d90; @@ -30,6 +30,7 @@ $gl-placeholder-color: #8f8f8f; $gl-icon-color: $gl-placeholder-color; $gl-grayish-blue: #7f8fa4; $gl-gray: $gl-text-color; +$gl-gray-dark: #313236; $gl-header-color: $gl-title-color; /* @@ -65,7 +66,7 @@ $gl-padding-top: 10px; $row-hover: #f4f8fe; $progress-color: #c0392b; $avatar_radius: 50%; -$header-height: 58px; +$header-height: 50px; $fixed-layout-width: 1280px; $gl-avatar-size: 40px; $error-exclamation-point: #e62958; @@ -74,6 +75,9 @@ $btn-transparent-color: #8f8f8f; $settings-icon-size: 18px; $provider-btn-group-border: #e5e5e5; $provider-btn-not-active-color: #4688f1; +$link-underline-blue: #4a8bee; +$layout-link-gray: #7e7c7c; +$todo-alert-blue: #428bca; /* * Color schema @@ -108,6 +112,7 @@ $red-light: #e52c5a; $red-normal: #d22852; $red-dark: darken($red-normal, 5%); +$black: #000; $black-transparent: rgba(0, 0, 0, 0.3); $border-white-light: #f1f2f4; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 201f3e5ca46124f5cd4e4913fe5f89febb98e560..aa41565f8128b9cb648aadee4c17bfb8bf09ccf9 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -83,3 +83,12 @@ } } } + +table.builds { + + .build-link { + a { + color: $gl-dark-link-color; + } + } +} diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index 358d2f4ab9d9d73c60880c62f0994701946e20d2..c2cd227571f3cfaf6521fbaed5e0054e7037efc5 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -31,9 +31,23 @@ } .commit-committer-link, .commit-author-link { - color: #444; + color: $gl-gray; font-weight: bold; } + + .time_ago { + margin-left: 8px; + } + + .fa-clipboard { + color: $dropdown-title-btn-color; + } + + .commit-info { + &.branches { + margin-left: 8px; + } + } } .commit-box { @@ -42,7 +56,7 @@ .commit-title { margin: 0; font-size: 23px; - color: #313236; + color: $gl-gray-dark; } .commit-description { @@ -83,6 +97,14 @@ } } +.commit-action-buttons { + i { + color: $gl-icon-color; + font-size: 13px; + margin-right: 3px; + } +} + /* * Commit message textarea for web editor and * custom merge request message diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 3438dbe4958aaa3fcfc75af90e9eed07d138957f..5e61e61d85cadd5e856db2cfe1e513310210d2d3 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -22,7 +22,7 @@ .title { margin: 0; font-size: 23px; - color: #313236; + color: $gl-gray-dark; } .description { diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index ee95bdf488e3842fe4c05446506ba2a4ac565420..4a95b7b852e0c21f623499033ccd823ced20282b 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/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 4ef548ffbe7c132c1e4c11d823a62c084a781f53..c4005ba1e69265308b44af7becf9cf909578bfb1 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -104,7 +104,7 @@ font-weight: 600; font-size: 17px; margin: 5px 0; - color: #313236; + color: $gl-gray-dark; } p:last-child { @@ -136,7 +136,7 @@ } .label-branch { - color: #313236; + color: $gl-gray-dark; font-family: $monospace_font; font-weight: bold; overflow: hidden; @@ -272,3 +272,19 @@ display: inline-block; width: 250px; } + +.table-holder { + .builds { + + th { + background-color: $white-light; + color: $gl-placeholder-color; + } + + th, + td { + padding: 16px; + } + } +} + diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index d0e72a4422c321f5edb32c9084ebac957b62dbe0..b94f524b51339b1f243358c2755788242c9c19e3 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -28,7 +28,7 @@ li.milestone { // Issue title span a { - color: rgba(0,0,0,0.64); + color: $gl-text-color; } } } @@ -51,7 +51,7 @@ li.milestone { margin-top: 7px; .issuable-number { - color: rgba(0,0,0,0.44); + color: $gl-placeholder-color; margin-right: 5px; } .avatar { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 9619d65db853c494921f3937114c14a7288e549e..624c8249f7e154dbc633c4176706c667017b3797 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -114,10 +114,6 @@ ul.notes { word-break: keep-all; } } - - a { - word-break: break-all; - } } .note-header { @@ -172,6 +168,11 @@ ul.notes { .notes { background-color: $white-light; } + + a code { + top: 0; + margin-right: 0; + } } } } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 01f984796235efac8a47d189b746e9a16ee1712b..abc5a0e9877377b0f7b15ff768734c2829605063 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -205,3 +205,21 @@ text-align: center; } } + +.user-profile { + @media (max-width: $screen-xs-max) { + .cover-block { + padding-top: 20px; + } + + .cover-controls { + position: static; + margin-bottom: 20px; + + .btn { + display: inline-block; + width: 48%; + } + } + } +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 99108e9bfc470fc676d4a61d5de71eb19e2b115c..c20f04653fcc9d0914d693a6574d443405a59dd9 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -178,7 +178,7 @@ .option-title { font-weight: normal; display: inline-block; - color: #313236; + color: $gl-gray-dark; } .option-descr { diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index dbb6daf0d7003544d873f3e0b007518953f176c5..2370d35924e9e51c02edc48965fd95d1c0cedaf7 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -1,7 +1,7 @@ .container-fluid { .ci-status { padding: 2px 7px; - margin-right: 5px; + margin-right: 10px; border: 1px solid #eee; white-space: nowrap; @include border-radius(4px); diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 75f78569e3c17df7f519c03b581509aa31f8c30e..e51c3491dae714048642fbd46ea6da4bab679d97 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -6,9 +6,16 @@ .navbar-nav { li { .badge.todos-pending-count { - background-color: $gl-icon-color; margin-top: -5px; font-weight: normal; + background: $todo-alert-blue; + margin-left: -17px; + font-size: 11px; + color: white; + padding: 3px; + padding-top: 1px; + padding-bottom: 1px; + border-radius: 3px; } } } diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 25b5e95583e12bb6f7e4258378a6dec12b6f5cfe..a84fc2e0318af423b4b3ff891ebd7cdae8612c4c 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -16,7 +16,7 @@ tr { > td, > th { - line-height: 26px; + line-height: 23px; } &:hover { diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb index 9083bfb41cfdb494dcba62e0bb6ec25186dbdfbd..cf795d977ce4ddf7b2d50e3fd98620e91bbd5406 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 93c4894ea0f5791d023c5d3f08eae9480abb8632..4e85b6b4cf200026c1207a68f98e14726dc16655 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 bf98af786158ce693956a442142a899b48d0cdf2..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..8be35f00a775f70e27ef584d56d1993477c42b6c --- /dev/null +++ b/app/controllers/admin/impersonations_controller.rb @@ -0,0 +1,26 @@ +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) + + Gitlab::AppLogger.info("User #{original_user.username} has stopped impersonating #{impersonator.username}") + + 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 9abf08d0e19a462d5b8f1c5664964ac300f82f40..f2f654c7bcdeb37c77454da89c266a3c850266e4 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -31,6 +31,24 @@ 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) + + Gitlab::AppLogger.info("User #{current_user.username} has started impersonating #{user.username}") + + 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/application_controller.rb b/app/controllers/application_controller.rb index 1c53b0b21a3695edbccbc15fec82d7e116fb347e..17b3f49aed1c82cfb825332a4af567f70e6eaf17 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -117,7 +117,7 @@ class ApplicationController < ActionController::Base end def after_sign_out_path_for(resource) - current_application_settings.after_sign_out_path || new_user_session_path + current_application_settings.after_sign_out_path.presence || new_user_session_path end def abilities diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 1420b96840cbf5c79e13034d87acc9ba69fdaf16..a52c614b259accce46bd0b82f36b0b4e4d3d8080 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -15,7 +15,7 @@ class Projects::CommitsController < Projects::ApplicationController if search.present? @repository.find_commits_by_message(search, @ref, @path, @limit, @offset).compact else - @repository.commits(@ref, @path, @limit, @offset) + @repository.commits(@ref, path: @path, limit: @limit, offset: @offset) end @note_counts = project.notes.where(commit_id: @commits.map(&:id)). diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb index d13ea9f34b664032a3e04b08ea047fce477b266c..092ef32e6e397d02a7bca135ecd4290c7494dbb3 100644 --- a/app/controllers/projects/graphs_controller.rb +++ b/app/controllers/projects/graphs_controller.rb @@ -17,7 +17,7 @@ class Projects::GraphsController < Projects::ApplicationController end def commits - @commits = @project.repository.commits(@ref, nil, 2000, 0, true) + @commits = @project.repository.commits(@ref, limit: 2000, skip_merges: true) @commits_graph = Gitlab::Graphs::Commits.new(@commits) @commits_per_week_days = @commits_graph.commits_per_week_days @commits_per_time = @commits_graph.commits_per_time @@ -55,7 +55,7 @@ class Projects::GraphsController < Projects::ApplicationController private def fetch_graph - @commits = @project.repository.commits(@ref, nil, 6000, 0, true) + @commits = @project.repository.commits(@ref, limit: 6000, skip_merges: true) @log = [] @commits.each do |commit| diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index 5fd4f855dec8d4073574d53d2b6aff161a64f158..dfa9bd259e80fba381369710ba145bf59fc405ac 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 9face235baa9518d1d43cd43fb3349b2740a4362..016f5dd0005bd7f784733e30d2541899cc507c78 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -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 diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index c02bc28acef46af91f3a653ab4e3d9c5fe92163f..0d6c32fabd255e11c92eb34fa05fc6bb442dfcab 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/registrations_controller.rb b/app/controllers/registrations_controller.rb index 059b88e22538895a0760dd72090d4f107bc0e5a2..352bff1938335734ba410866ad4089119be275b2 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -8,6 +8,13 @@ class RegistrationsController < Devise::RegistrationsController def create if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha + # To avoid duplicate form fields on the login page, the registration form + # names fields using `new_user`, but Devise still wants the params in + # `user`. + if params["new_#{resource_name}"].present? && params[resource_name].blank? + params[resource_name] = params.delete(:"new_#{resource_name}") + end + super else flash[:alert] = "There was an error with the reCAPTCHA code below. Please re-enter the code." diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index a41172816b817515d01bc7a3a98af0068ce2c8ba..01cbf91c658b8a9a5d7a2d2652dce5eba14925ef 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 474c6f27374e5573147d2931abc4f2e0a4abe71c..93241b3afb7762caee9157a13a9ff7667ead1f01 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -131,7 +131,7 @@ module BlobHelper # elements and attributes. Note that this whitelist is by no means complete # and may omit some elements. def sanitize_svg(blob) - blob.data = Loofah.scrub_fragment(blob.data, :strip).to_xml + blob.data = Gitlab::Sanitizers::SVG.clean(blob.data) blob end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index afe1e11a0da6ad5cb9f576cd309e63d9d6bf9fc0..198d39455d7651333992af7cfdcbb6a41ad9b12e 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/nav_helper.rb b/app/helpers/nav_helper.rb index 5d86bd490a8675469a60669f9daf7fe58559dd18..3aa41030453b88a9754b0b76d12140a6e0545a58 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -34,10 +34,13 @@ module NavHelper end def nav_header_class - if nav_menu_collapsed? - "header-collapsed" - else - "header-expanded" - end + class_name = + if nav_menu_collapsed? + "header-collapsed" + else + "header-expanded" + end + class_name += " with-horizontal-nav" if defined?(nav) && nav + class_name end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 3d5e61d2c18706cc51718d2412450edf20b99f1d..f17d02a7a3f49c1768789a8e70df6f53e92b0297 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -200,12 +200,8 @@ module ProjectsHelper end def repository_size(project = @project) - "#{project.repository_size} MB" - rescue - # In order to prevent 500 error - # when application cannot allocate memory - # to calculate repo size - just show 'Unknown' - 'unknown' + size_in_bytes = project.repository_size * 1.megabyte + number_to_human_size(size_in_bytes, delimiter: ',', precision: 2) end def default_url_to_repo(project = @project) @@ -341,4 +337,10 @@ module ProjectsHelper ) end end + + def sanitize_repo_path(message) + return '' unless message.present? + + message.strip.gsub(Gitlab.config.gitlab_shell.repos_path.chomp('/'), "[REPOS PATH]") + end end diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index cdc40b81ee1b0757a7794c6ed32bd75014d33304..96116e916dd3cd807413029ee1a392de1f85f31f 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/appearance.rb b/app/models/appearance.rb index 4cf8dd9a8ce2f3b4d5519eb4646d347004a808fc..4528760fefabdf79ff365fca8fdc77eb11021155 100644 --- a/app/models/appearance.rb +++ b/app/models/appearance.rb @@ -1,3 +1,16 @@ +# == Schema Information +# +# Table name: appearances +# +# id :integer not null, primary key +# title :string +# description :text +# header_logo :string +# logo :string +# created_at :datetime not null +# updated_at :datetime not null +# + class Appearance < ActiveRecord::Base validates :title, presence: true validates :description, presence: true diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 36f88154232319605450d3b84beac6878c3e8210..72ec91d29092d4041be14b017df6469d400ea5c3 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -10,21 +10,20 @@ # sign_in_text :text # created_at :datetime # updated_at :datetime -# home_page_url :string(255) +# home_page_url :string # default_branch_protection :integer default(2) # restricted_visibility_levels :text # version_check_enabled :boolean default(TRUE) # max_attachment_size :integer default(10), not null # default_project_visibility :integer # default_snippet_visibility :integer -# default_group_visibility :integer # restricted_signup_domains :text # user_oauth_applications :boolean default(TRUE) -# after_sign_out_path :string(255) +# after_sign_out_path :string # session_expire_delay :integer default(10080), not null # import_sources :text # help_page_text :text -# admin_notification_email :string(255) +# admin_notification_email :string # shared_runners_enabled :boolean default(TRUE), not null # max_artifacts_size :integer default(100), not null # runners_registration_token :string @@ -32,8 +31,6 @@ # two_factor_grace_period :integer default(48) # metrics_enabled :boolean default(FALSE) # metrics_host :string default("localhost") -# metrics_username :string -# metrics_password :string # metrics_pool_size :integer default(16) # metrics_timeout :integer default(10) # metrics_method_call_threshold :integer default(10) @@ -41,9 +38,16 @@ # recaptcha_site_key :string # recaptcha_private_key :string # metrics_port :integer default(8089) +# metrics_sample_interval :integer default(15) # sentry_enabled :boolean default(FALSE) # sentry_dsn :string +# akismet_enabled :boolean default(FALSE) +# akismet_api_key :string # email_author_in_body :boolean default(FALSE) +# default_group_visibility :integer +# repository_checks_enabled :boolean default(FALSE) +# metrics_packet_size :integer default(1) +# shared_runners_text :text # class ApplicationSetting < ActiveRecord::Base diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb index 0ed0dd98a5950437930ea3f5c64fd2079b615cb4..44b090260e755a6546154c21542b9fa6bf0b429c 100644 --- a/app/models/audit_event.rb +++ b/app/models/audit_event.rb @@ -4,9 +4,9 @@ # # id :integer not null, primary key # author_id :integer not null -# type :string(255) not null +# type :string not null # entity_id :integer not null -# entity_type :string(255) not null +# entity_type :string not null # details :text # created_at :datetime # updated_at :datetime diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb index 8a0a8a4c2a9b846936574a68e5e7c12835169714..075ac733bfcccdf67587cbff72195da5069f4997 100644 --- a/app/models/broadcast_message.rb +++ b/app/models/broadcast_message.rb @@ -8,8 +8,8 @@ # ends_at :datetime # created_at :datetime # updated_at :datetime -# color :string(255) -# font :string(255) +# color :string +# font :string # class BroadcastMessage < ActiveRecord::Base diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index c2ddee527e5ee6677733188ae881dc59a60af270..2fea8047147615569601b44e9299069bbfcc27db 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -4,7 +4,7 @@ # # id :integer not null, primary key # project_id :integer -# status :string(255) +# status :string # finished_at :datetime # trace :text # created_at :datetime @@ -15,19 +15,19 @@ # commit_id :integer # commands :text # job_id :integer -# name :string(255) +# name :string # deploy :boolean default(FALSE) # options :text # allow_failure :boolean default(FALSE), not null -# stage :string(255) +# stage :string # trigger_request_id :integer # stage_idx :integer # tag :boolean -# ref :string(255) +# ref :string # user_id :integer -# type :string(255) -# target_url :string(255) -# description :string(255) +# type :string +# target_url :string +# description :string # artifacts_file :text # gl_project_id :integer # artifacts_metadata :text diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index f2667e5476b7896d66fede910ddf1ad83f844914..4ac4e0fb8b2250827685b556f68b02e19953ca46 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -4,9 +4,9 @@ # # id :integer not null, primary key # project_id :integer -# ref :string(255) -# sha :string(255) -# before_sha :string(255) +# ref :string +# sha :string +# before_sha :string # push_data :text # created_at :datetime # updated_at :datetime @@ -14,6 +14,10 @@ # yaml_errors :text # committed_at :datetime # gl_project_id :integer +# status :string +# started_at :datetime +# finished_at :datetime +# duration :integer # module Ci diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 90349a07594966e60529a79aa37647eb837ab6a2..add59a08892f7dc34306885c19f0bd5da22b507d 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -3,18 +3,18 @@ # Table name: ci_runners # # id :integer not null, primary key -# token :string(255) +# token :string # created_at :datetime # updated_at :datetime -# description :string(255) +# description :string # contacted_at :datetime # active :boolean default(TRUE), not null # is_shared :boolean default(FALSE) -# name :string(255) -# version :string(255) -# revision :string(255) -# platform :string(255) -# architecture :string(255) +# name :string +# version :string +# revision :string +# platform :string +# architecture :string # module Ci diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index 2b9a457c8ab6f32235d5f661056682c14d9089c3..4f3f4d79fac836537b5543b19b40853b134fb964 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -3,7 +3,7 @@ # Table name: ci_triggers # # id :integer not null, primary key -# token :string(255) +# token :string # project_id :integer # deleted_at :datetime # created_at :datetime diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index e786bd7dd93c99f9cb4ddc0a588f2c9b0a9d1856..4229fe085a15875c78318a0f336314463e54c565 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -4,11 +4,11 @@ # # id :integer not null, primary key # project_id :integer -# key :string(255) +# key :string # value :text # encrypted_value :text -# encrypted_value_salt :string(255) -# encrypted_value_iv :string(255) +# encrypted_value_salt :string +# encrypted_value_iv :string # gl_project_id :integer # diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index aa56314aa164a03886d55fa93dbc3e8cf27117fc..1260c448de3533420d18798373bab04e0e3077f4 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -4,7 +4,7 @@ # # id :integer not null, primary key # project_id :integer -# status :string(255) +# status :string # finished_at :datetime # trace :text # created_at :datetime @@ -15,21 +15,24 @@ # commit_id :integer # commands :text # job_id :integer -# name :string(255) +# name :string # deploy :boolean default(FALSE) # options :text # allow_failure :boolean default(FALSE), not null -# stage :string(255) +# stage :string # trigger_request_id :integer # stage_idx :integer # tag :boolean -# ref :string(255) +# ref :string # user_id :integer -# type :string(255) -# target_url :string(255) -# description :string(255) +# type :string +# target_url :string +# description :string # artifacts_file :text # gl_project_id :integer +# artifacts_metadata :text +# erased_by_id :integer +# erased_at :datetime # class CommitStatus < ActiveRecord::Base diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index d5166e814743693c82c5b638f7acccc057c121cf..2e4efc4e8d8101cf51969ad9441f75b67f7ba7b2 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -35,13 +35,14 @@ module Issuable scope :only_opened, -> { with_state(:opened) } scope :only_reopened, -> { with_state(:reopened) } scope :closed, -> { with_state(:closed) } - scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') } - scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') } + scope :order_milestone_due_desc, -> { outer_join_milestone.reorder('milestones.due_date IS NULL ASC, milestones.due_date DESC, milestones.id DESC') } + scope :order_milestone_due_asc, -> { outer_join_milestone.reorder('milestones.due_date IS NULL ASC, milestones.due_date ASC, milestones.id ASC') } scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) } scope :join_project, -> { joins(:project) } scope :references_project, -> { references(:project) } scope :non_archived, -> { join_project.where(projects: { archived: false }) } + scope :outer_join_milestone, -> { joins("LEFT OUTER JOIN milestones ON milestones.id = #{table_name}.milestone_id") } delegate :name, :email, diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb index 9ab663c04ad926be03cf412eed596472f0885d5b..43cf625f770aaf901c9d1421e20c1dd3df4a2c0b 100644 --- a/app/models/deploy_key.rb +++ b/app/models/deploy_key.rb @@ -7,9 +7,9 @@ # created_at :datetime # updated_at :datetime # key :text -# title :string(255) -# type :string(255) -# fingerprint :string(255) +# title :string +# type :string +# fingerprint :string # public :boolean default(FALSE), not null # diff --git a/app/models/email.rb b/app/models/email.rb index b323d1edd100c53d8fb58a838732959c8396ba63..eae2472f33765260923d57b7791f0b282d859e49 100644 --- a/app/models/email.rb +++ b/app/models/email.rb @@ -4,7 +4,7 @@ # # id :integer not null, primary key # user_id :integer not null -# email :string(255) not null +# email :string not null # created_at :datetime # updated_at :datetime # diff --git a/app/models/event.rb b/app/models/event.rb index 897518aadc773b4fab1ca8793ebdc8a324a61997..25c7c3e6dc7814051c4d289a2b20794dd57f4e08 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -3,9 +3,9 @@ # Table name: events # # id :integer not null, primary key -# target_type :string(255) +# target_type :string # target_id :integer -# title :string(255) +# title :string # data :text # project_id :integer # created_at :datetime diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb index 97f4f03a9a546fb9ec15c84dc46b97bee3947756..d4afd8cbe84085d5e61107de693e2e17f93f7a74 100644 --- a/app/models/generic_commit_status.rb +++ b/app/models/generic_commit_status.rb @@ -4,7 +4,7 @@ # # id :integer not null, primary key # project_id :integer -# status :string(255) +# status :string # finished_at :datetime # trace :text # created_at :datetime @@ -15,21 +15,24 @@ # commit_id :integer # commands :text # job_id :integer -# name :string(255) +# name :string # deploy :boolean default(FALSE) # options :text # allow_failure :boolean default(FALSE), not null -# stage :string(255) +# stage :string # trigger_request_id :integer # stage_idx :integer # tag :boolean -# ref :string(255) +# ref :string # user_id :integer -# type :string(255) -# target_url :string(255) -# description :string(255) +# type :string +# target_url :string +# description :string # artifacts_file :text # gl_project_id :integer +# artifacts_metadata :text +# erased_by_id :integer +# erased_at :datetime # class GenericCommitStatus < CommitStatus diff --git a/app/models/group.rb b/app/models/group.rb index 1f8432e3320222e0904c5780fb963d0738680ab8..cff76877958f6ceb971ea383893a43673a9348c5 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -2,16 +2,17 @@ # # Table name: namespaces # -# id :integer not null, primary key -# name :string(255) not null -# path :string(255) not null -# owner_id :integer -# visibility_level :integer default(20), not null -# created_at :datetime -# updated_at :datetime -# type :string(255) -# description :string(255) default(""), not null -# avatar :string(255) +# id :integer not null, primary key +# name :string not null +# path :string not null +# owner_id :integer +# created_at :datetime +# updated_at :datetime +# type :string +# description :string default(""), not null +# avatar :string +# share_with_group_lock :boolean default(FALSE) +# visibility_level :integer default(20), not null # require 'carrierwave/orm/activerecord' diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index bc6e0f98c3c5ebb1bd24560f29463cbd88be1778..2b8f34a056844f3a9c9fa207c8d7b29750c6249f 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -16,6 +16,8 @@ # note_events :boolean default(FALSE), not null # enable_ssl_verification :boolean default(TRUE) # build_events :boolean default(FALSE), not null +# wiki_page_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 80962264ba2c4b398a492521f5ac8dc60c636ba7..0e176de5ef81b8e1e65b685ab06de4aaae87f068 100644 --- a/app/models/hooks/service_hook.rb +++ b/app/models/hooks/service_hook.rb @@ -16,6 +16,8 @@ # note_events :boolean default(FALSE), not null # enable_ssl_verification :boolean default(TRUE) # build_events :boolean default(FALSE), not null +# wiki_page_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 15dddcc2447f517894535810618dc219ac66c438..ad508cbbcb8a7429eb6ec1452b50bb0a985bca62 100644 --- a/app/models/hooks/system_hook.rb +++ b/app/models/hooks/system_hook.rb @@ -16,6 +16,8 @@ # note_events :boolean default(FALSE), not null # enable_ssl_verification :boolean default(TRUE) # build_events :boolean default(FALSE), not null +# wiki_page_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 3a2e4f546f72e4c7c7ebacd7e16e4213e7a93e44..8e58c9583abadae05207bd6a4278eea1a97cb7d9 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -16,6 +16,8 @@ # note_events :boolean default(FALSE), not null # enable_ssl_verification :boolean default(TRUE) # build_events :boolean default(FALSE), not null +# wiki_page_events :boolean default(FALSE), not null +# token :string # class WebHook < ActiveRecord::Base @@ -43,23 +45,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 +69,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/identity.rb b/app/models/identity.rb index e1915b079d439d5c954ab04a33f5474700b3aa3d..ef4d5f99091bf16d622ed8e516d5fa142fc2e8ff 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -3,8 +3,8 @@ # Table name: identities # # id :integer not null, primary key -# extern_uid :string(255) -# provider :string(255) +# extern_uid :string +# provider :string # user_id :integer # created_at :datetime # updated_at :datetime diff --git a/app/models/issue.rb b/app/models/issue.rb index ea1bfb776ee26fec7d58044373e916d9c7154dca..abaa509707c15d0f81ff3f7cd4b4a20c717fbc7a 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -3,20 +3,23 @@ # Table name: issues # # id :integer not null, primary key -# title :string(255) +# title :string # assignee_id :integer # author_id :integer # project_id :integer # created_at :datetime # updated_at :datetime # position :integer default(0) -# branch_name :string(255) +# branch_name :string # description :text # milestone_id :integer -# state :string(255) +# state :string # iid :integer # updated_by_id :integer # moved_to_id :integer +# confidential :boolean default(FALSE) +# deleted_at :datetime +# due_date :date # require 'carrierwave/orm/activerecord' diff --git a/app/models/key.rb b/app/models/key.rb index 0282ad1813915ee14d7740ba8ef337f9a5c003cc..b2b57849f8a55a46c1a71ba536e9e268309a9ceb 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -7,9 +7,9 @@ # created_at :datetime # updated_at :datetime # key :text -# title :string(255) -# type :string(255) -# fingerprint :string(255) +# title :string +# type :string +# fingerprint :string # public :boolean default(FALSE), not null # diff --git a/app/models/label.rb b/app/models/label.rb index 60bdce32952ed15791d076cb00dd02b3ce3e98e4..9a22398d9523f4809b178721196f4a97cc13e9f1 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -2,14 +2,14 @@ # # Table name: labels # -# id :integer not null, primary key -# title :string(255) -# color :string(255) -# project_id :integer -# created_at :datetime -# updated_at :datetime -# template :boolean default(FALSE) -# description :string(255) +# id :integer not null, primary key +# title :string +# color :string +# project_id :integer +# created_at :datetime +# updated_at :datetime +# template :boolean default(FALSE) +# description :string # class Label < ActiveRecord::Base diff --git a/app/models/label_link.rb b/app/models/label_link.rb index b94c9c777af3109afe8cb86712063451c2c51dc0..7b8e872b6dd1934f45ea855bdb78df186cc364c3 100644 --- a/app/models/label_link.rb +++ b/app/models/label_link.rb @@ -5,7 +5,7 @@ # id :integer not null, primary key # label_id :integer # target_id :integer -# target_type :string(255) +# target_type :string # created_at :datetime # updated_at :datetime # diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index 86b1b7e2f99839408e84502c5231d0649ef6aae3..927e764af926983edb02151fb8bfaf33351096be 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -3,11 +3,11 @@ # Table name: lfs_objects # # id :integer not null, primary key -# oid :string(255) not null +# oid :string not null # size :integer not null # created_at :datetime # updated_at :datetime -# file :string(255) +# file :string # class LfsObject < ActiveRecord::Base diff --git a/app/models/member.rb b/app/models/member.rb index 60efafef211791648a2420a7988a359873f0ed58..cca82da89f18797eb70b94d6b408c02d197e44a0 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -5,15 +5,15 @@ # id :integer not null, primary key # access_level :integer not null # source_id :integer not null -# source_type :string(255) not null +# source_type :string not null # user_id :integer # notification_level :integer not null -# type :string(255) +# type :string # created_at :datetime # updated_at :datetime # created_by_id :integer -# invite_email :string(255) -# invite_token :string(255) +# invite_email :string +# invite_token :string # invite_accepted_at :datetime # diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 9fb474a1a93d8fd54dc278d789cfa13485c1c8af..a48c1943e6f86d2240b172d6dfa945f19aa0410f 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -5,15 +5,15 @@ # id :integer not null, primary key # access_level :integer not null # source_id :integer not null -# source_type :string(255) not null +# source_type :string not null # user_id :integer # notification_level :integer not null -# type :string(255) +# type :string # created_at :datetime # updated_at :datetime # created_by_id :integer -# invite_email :string(255) -# invite_token :string(255) +# invite_email :string +# invite_token :string # invite_accepted_at :datetime # diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 07ddb02ae9dd6bba8da9b3ea4947d222ca60bc83..143350a0b55d8e1173c7eb0c05812da6f7279621 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -5,15 +5,15 @@ # id :integer not null, primary key # access_level :integer not null # source_id :integer not null -# source_type :string(255) not null +# source_type :string not null # user_id :integer # notification_level :integer not null -# type :string(255) +# type :string # created_at :datetime # updated_at :datetime # created_by_id :integer -# invite_email :string(255) -# invite_token :string(255) +# invite_email :string +# invite_token :string # invite_accepted_at :datetime # diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index d00919c3b0c10ce0a2943561283c0354f8d5fcd3..4175e1e5fbabb9be841be4d40300b4a10806e763 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -3,28 +3,29 @@ # Table name: merge_requests # # id :integer not null, primary key -# target_branch :string(255) not null -# source_branch :string(255) not null +# target_branch :string not null +# source_branch :string not null # source_project_id :integer not null # author_id :integer # assignee_id :integer -# title :string(255) +# title :string # created_at :datetime # updated_at :datetime # milestone_id :integer -# state :string(255) -# merge_status :string(255) +# state :string +# merge_status :string # target_project_id :integer not null # iid :integer # description :text # position :integer default(0) # locked_at :datetime # updated_by_id :integer -# merge_error :string(255) +# merge_error :string # merge_params :text # merge_when_build_succeeds :boolean default(FALSE), not null # merge_user_id :integer # merge_commit_sha :string +# deleted_at :datetime # class MergeRequest < ActiveRecord::Base diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 0580cafdd1b1cb89d978e809a8cb3fb02fbb4ee2..8951e92a0b87dc8564af0147bf98c36087b1e386 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -3,12 +3,14 @@ # Table name: merge_request_diffs # # id :integer not null, primary key -# state :string(255) +# state :string # st_commits :text # st_diffs :text # merge_request_id :integer not null # created_at :datetime # updated_at :datetime +# base_commit_sha :string +# real_size :string # class MergeRequestDiff < ActiveRecord::Base diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 986184dd3019d22cbf05013bed320792b5b7c8fb..5ee8a965ad8beafc090652ffcbbb457445a24f8e 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -3,13 +3,13 @@ # Table name: milestones # # id :integer not null, primary key -# title :string(255) not null +# title :string not null # project_id :integer not null # description :text # due_date :date # created_at :datetime # updated_at :datetime -# state :string(255) +# state :string # iid :integer # diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 55842df1e2d3138919a52d4984c62ddd2e593d20..741e912171d5d2b546ae134b791b70e459d6a2d2 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -2,15 +2,17 @@ # # Table name: namespaces # -# id :integer not null, primary key -# name :string(255) not null -# path :string(255) not null -# owner_id :integer -# created_at :datetime -# updated_at :datetime -# type :string(255) -# description :string(255) default(""), not null -# avatar :string(255) +# id :integer not null, primary key +# name :string not null +# path :string not null +# owner_id :integer +# created_at :datetime +# updated_at :datetime +# type :string +# description :string default(""), not null +# avatar :string +# share_with_group_lock :boolean default(FALSE) +# visibility_level :integer default(20), not null # class Namespace < ActiveRecord::Base diff --git a/app/models/note.rb b/app/models/note.rb index 71b4293d57a593fb98896fd4a49b6dee79571575..deee2b9e8854cf6aa64c75684dfbaaf85b5bc497 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -4,14 +4,14 @@ # # id :integer not null, primary key # note :text -# noteable_type :string(255) +# noteable_type :string # author_id :integer # created_at :datetime # updated_at :datetime # project_id :integer -# attachment :string(255) -# line_code :string(255) -# commit_id :string(255) +# attachment :string +# line_code :string +# commit_id :string # noteable_id :integer # system :boolean default(FALSE), not null # st_diff :text diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 5001738f411f43a05c678d7edf6d0fd85bbda8d6..846773752a67f03b6f9946e5cf23f8b40ff88fcc 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -1,3 +1,16 @@ +# == Schema Information +# +# Table name: notification_settings +# +# id :integer not null, primary key +# user_id :integer not null +# source_id :integer not null +# source_type :string not null +# level :integer default(0), not null +# created_at :datetime not null +# updated_at :datetime not null +# + class NotificationSetting < ActiveRecord::Base enum level: { disabled: 0, participating: 1, watch: 2, global: 3, mention: 4 } diff --git a/app/models/personal_snippet.rb b/app/models/personal_snippet.rb index 452f3913eef66cdb897ceae9bbd720e302dba8ee..1d5f4c50254f6fa624fc75b8f584199d37371178 100644 --- a/app/models/personal_snippet.rb +++ b/app/models/personal_snippet.rb @@ -3,14 +3,14 @@ # Table name: snippets # # id :integer not null, primary key -# title :string(255) +# title :string # content :text # author_id :integer not null # project_id :integer # created_at :datetime # updated_at :datetime -# file_name :string(255) -# type :string(255) +# file_name :string +# type :string # visibility_level :integer default(0), not null # diff --git a/app/models/project.rb b/app/models/project.rb index 76265a59ea7bab94f4f5d122e946e18009362474..bce2545537305cfe7c6d0cef0a55862b81efeac7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2,41 +2,46 @@ # # Table name: projects # -# id :integer not null, primary key -# name :string(255) -# path :string(255) -# description :text -# created_at :datetime -# updated_at :datetime -# creator_id :integer -# issues_enabled :boolean default(TRUE), not null -# wall_enabled :boolean default(TRUE), not null -# merge_requests_enabled :boolean default(TRUE), not null -# wiki_enabled :boolean default(TRUE), not null -# namespace_id :integer -# issues_tracker :string(255) default("gitlab"), not null -# issues_tracker_id :string(255) -# snippets_enabled :boolean default(TRUE), not null -# last_activity_at :datetime -# import_url :string(255) -# visibility_level :integer default(0), not null -# archived :boolean default(FALSE), not null -# avatar :string(255) -# import_status :string(255) -# repository_size :float default(0.0) -# star_count :integer default(0), not null -# import_type :string(255) -# import_source :string(255) -# commit_count :integer default(0) -# import_error :text -# ci_id :integer -# builds_enabled :boolean default(TRUE), not null -# shared_runners_enabled :boolean default(TRUE), not null -# runners_token :string -# build_coverage_regex :string -# build_allow_git_fetch :boolean default(TRUE), not null -# build_timeout :integer default(3600), not null -# pending_delete :boolean +# id :integer not null, primary key +# name :string +# path :string +# description :text +# created_at :datetime +# updated_at :datetime +# creator_id :integer +# issues_enabled :boolean default(TRUE), not null +# wall_enabled :boolean default(TRUE), not null +# merge_requests_enabled :boolean default(TRUE), not null +# wiki_enabled :boolean default(TRUE), not null +# namespace_id :integer +# issues_tracker :string default("gitlab"), not null +# issues_tracker_id :string +# snippets_enabled :boolean default(TRUE), not null +# last_activity_at :datetime +# import_url :string +# visibility_level :integer default(0), not null +# archived :boolean default(FALSE), not null +# avatar :string +# import_status :string +# repository_size :float default(0.0) +# star_count :integer default(0), not null +# import_type :string +# import_source :string +# commit_count :integer default(0) +# import_error :text +# ci_id :integer +# builds_enabled :boolean default(TRUE), not null +# shared_runners_enabled :boolean default(TRUE), not null +# runners_token :string +# build_coverage_regex :string +# build_allow_git_fetch :boolean default(TRUE), not null +# build_timeout :integer default(3600), not null +# pending_delete :boolean default(FALSE) +# public_builds :boolean default(TRUE), not null +# main_language :string +# pushes_since_gc :integer default(0) +# last_repository_check_failed :boolean +# last_repository_check_at :datetime # require 'carrierwave/orm/activerecord' @@ -740,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) @@ -769,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) @@ -1041,4 +1044,11 @@ class Project < ActiveRecord::Base def wiki @wiki ||= ProjectWiki.new(self, self.owner) end + + def schedule_delete!(user_id, params) + # Queue this task for after the commit, so once we mark pending_delete it will run + run_after_commit { ProjectDestroyWorker.perform_async(id, user_id, params) } + + update_attribute(:pending_delete, true) + end end diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb index e52a6bd7c8473b69054824db5724ae4d12d77954..66f5a609bf5f3035a1f986e092472fb0e4cf88d3 100644 --- a/app/models/project_group_link.rb +++ b/app/models/project_group_link.rb @@ -1,3 +1,15 @@ +# == Schema Information +# +# Table name: project_group_links +# +# id :integer not null, primary key +# project_id :integer not null +# group_id :integer not null +# created_at :datetime +# updated_at :datetime +# group_access :integer default(30), not null +# + class ProjectGroupLink < ActiveRecord::Base GUEST = 10 REPORTER = 20 diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb index 2c0ae312f1babbf034f49b686d224d3dcff5e471..7830f764ed3f183c09ed644b5108ddfb7f681fe8 100644 --- a/app/models/project_import_data.rb +++ b/app/models/project_import_data.rb @@ -2,9 +2,12 @@ # # Table name: project_import_data # -# id :integer not null, primary key -# project_id :integer -# data :text +# id :integer not null, primary key +# project_id :integer +# data :text +# encrypted_credentials :text +# encrypted_credentials_iv :string +# encrypted_credentials_salt :string # require 'carrierwave/orm/activerecord' diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index 792ad804575841ea64200a967e96d083883e607f..368485a060aa8e433b536d7512e4220ff2ca5766 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # require 'asana' diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb index 29d841faed855b2180c4d8719a19732e6a6ef9d7..ffb7455b0143db3f819977ee441b95adcfb0877c 100644 --- a/app/models/project_services/assembla_service.rb +++ b/app/models/project_services/assembla_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # class AssemblaService < Service diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index 060062aaf7ad94bf96a77f8980cebcbbf1042b07..c36ee95e37801ef34a43bca57d245bf50284d7b9 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # class BambooService < CiService diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index 3efbfd2eec3abbd68b318d0a0701485476fdd6de..f9f4897a065f02117983564936f731db81f38dda 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # require "addressable/uri" @@ -26,7 +29,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 +94,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/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index 6ab6d7417b715ef7afaca937b368e83d8122d2b3..20cdfcaffb210e1e0b38621b15197b6253eca084 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # class BuildsEmailService < Service diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index 6e8f08425249352c65fe568264f8237cb47fdf69..28c969fe57f3f170d818ba7e95965a8549f58368 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # class CampfireService < Service diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index d9f0849d1470b5f316c35a49540f9b69b1df8214..9bc8f982da614a95239431ae33e665ad5f089491 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # # Base class for CI services diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb index 88a3e9218cb407ca7e6d44e4c43fa4c2ded6b49d..4d1319eb6f8329f0a8a5390bdf753bc48e548343 100644 --- a/app/models/project_services/custom_issue_tracker_service.rb +++ b/app/models/project_services/custom_issue_tracker_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # class CustomIssueTrackerService < IssueTrackerService diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index b4724bb647e7e73260c88230920a9b2585d34da0..d8e00e018cc5627de1d30213b7f9f6575e59fe72 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # class DroneCiService < CiService diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index b831577cd97a614bea8e5e8631f268df1059943e..2dbd29062dfac65a79aa2c878c07d68a8d044eff 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # class EmailsOnPushService < Service diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb index b402b68665af18ba6c9151f2b20629690f99ccdd..5469049bb5eb74a91152efca355772133fc4e30a 100644 --- a/app/models/project_services/external_wiki_service.rb +++ b/app/models/project_services/external_wiki_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # class ExternalWikiService < Service diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index 8605ce66e48ba2f8666363ad35ce4e41fed9dfc6..3dc1e0fbe8b9ca75a26385a2a3d5e65c81ee5f99 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # require "flowdock-git-hook" diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index 61babe9cfe504f940b97b55b13951ff00d4bbc60..b4c311cf664d9223bab202f3eb43cb5ff69c90da 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # require "gemnasium/gitlab_service" diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index 33f0d7ea01acb43d1436087bb9b1d2ae2aeb0363..a92f722608388ee5158eef8eda23680fac3cbefd 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # # TODO(ayufan): The GitLabCiService is deprecated and the type should be removed when the database entries are removed diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index eaa5654b9c6f7f8e237edfb57b05442deecb04f3..1adaeeb3b2baa5b9c6a5db7ad703e34faef6da1e 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # class GitlabIssueTrackerService < IssueTrackerService diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 064ef8e8674a80f7ca5a0e3d4dead37d4cd47302..f9ddf5887226ddd120ca39f6390d34ad3682e4ea 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # class HipchatService < Service diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index 04c714bfaad4fddee9d29a4fd0ecb4591a26e0af..b9a592d709635b46d4cfe53d4028877da5d6e964 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # require 'uri' diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 25045224ce5ac2c47b15ee17ca45f4ed6ca47f78..98a3a7c6b86f3c871a6e38aa9cb8e0fa77e291d6 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,11 +17,14 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # 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 1ed42c4f3e767b36de1c198a3a2bd2753717ad19..ba68658f0bd16229f3a2ea8d283773d21e5d7216 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # class JiraService < IssueTrackerService @@ -28,6 +31,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/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index c9a890c7e3f5a8a90dda602cbf8ac9ad6f5fae7b..acaa0c393651d291c8d2f35ed15a25368c99d96e 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # class PivotaltrackerService < Service diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index e76d9eca2abadba4bc03d97652440f359628004c..a640c8cb4404c2b3e3b1d790dd6cd53312720691 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # class PushoverService < Service diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index de974354c77f7370934c7138b93bc13e6b7923dd..e2137e92c6279872ab21fdbe193da2661232a62c 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # class RedmineService < IssueTrackerService diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index fd65027f08476a6cebd1170a8f4a9f30b2c9b3eb..83ffa53a407e35088c347fa5975a76e2a0673e28 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,12 +17,15 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # 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_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index 8dceee5e2c50c0f05f9b7e7765b9a9ffd33251fe..4015da31509a72ff2ce7667598e1264b708ba9d4 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # class TeamcityService < CiService diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb index 1f7d85a5f3d4b2e2f85ba8b10a1331baa31e6595..b4b2807eba43ec258e862825145c2ea655082e3f 100644 --- a/app/models/project_snippet.rb +++ b/app/models/project_snippet.rb @@ -3,14 +3,14 @@ # Table name: snippets # # id :integer not null, primary key -# title :string(255) +# title :string # content :text # author_id :integer not null # project_id :integer # created_at :datetime # updated_at :datetime -# file_name :string(255) -# type :string(255) +# file_name :string +# type :string # visibility_level :integer default(0), not null # @@ -22,4 +22,6 @@ class ProjectSnippet < Snippet # Scopes scope :fresh, -> { order("created_at DESC") } + + participant :author, :notes end diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 8ebd790a89e1be20e12c4a6764f1cc4709a4441e..3d2052c892ca75b8cdfa0829065362475c33af37 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -4,7 +4,7 @@ # # id :integer not null, primary key # project_id :integer not null -# name :string(255) not null +# name :string not null # created_at :datetime # updated_at :datetime # developers_can_push :boolean default(FALSE), not null diff --git a/app/models/release.rb b/app/models/release.rb index 89f70278af5bac889e5af3aa79a4a0de9e8bbbc6..dc700d1ea5a42c34fbc10cc42c5bb0372b3b92c5 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -3,7 +3,7 @@ # Table name: releases # # id :integer not null, primary key -# tag :string(255) +# tag :string # description :text # project_id :integer # created_at :datetime diff --git a/app/models/repository.rb b/app/models/repository.rb index b4319297e431fb6ff6dece478cc2f370d664a230..7aebfe279fb5f27fcf84eb2497f2086747ba7d39 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -87,13 +87,15 @@ class Repository nil end - def commits(ref, path = nil, limit = nil, offset = nil, skip_merges = false) + def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil) options = { repo: raw_repository, ref: ref, path: path, limit: limit, offset: offset, + after: after, + before: before, # --follow doesn't play well with --skip. See: # https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520 follow: false, @@ -146,10 +148,20 @@ class Repository find_branch(branch_name) end - def add_tag(tag_name, ref, message = nil) - before_push_tag + def add_tag(user, tag_name, target, message = nil) + oldrev = Gitlab::Git::BLANK_SHA + ref = Gitlab::Git::TAG_REF_PREFIX + tag_name + target = commit(target).try(:id) + + return false unless target + + options = { message: message, tagger: user_to_committer(user) } if message + + GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do + rugged.tags.create(tag_name, target, options) + end - gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message) + find_tag(tag_name) end def rm_branch(user, branch_name) @@ -575,7 +587,7 @@ class Repository end def contributors - commits = self.commits(nil, nil, 2000, 0, true) + commits = self.commits(nil, limit: 2000, offset: 0, skip_merges: true) commits.group_by(&:author_email).map do |email, commits| contributor = Gitlab::Contributor.new diff --git a/app/models/security_event.rb b/app/models/security_event.rb index 68c00adad5971078ab1ab5886864accef769995c..0bee03974f14521506935e746de0257cb499f50e 100644 --- a/app/models/security_event.rb +++ b/app/models/security_event.rb @@ -4,9 +4,9 @@ # # id :integer not null, primary key # author_id :integer not null -# type :string(255) not null +# type :string not null # entity_id :integer not null -# entity_type :string(255) not null +# entity_type :string not null # details :text # created_at :datetime # updated_at :datetime diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb index 77115597d71a4e90e422e02c87ba44215307339f..99279a2e083082a3412a41d9181c52e33ad7f404 100644 --- a/app/models/sent_notification.rb +++ b/app/models/sent_notification.rb @@ -5,11 +5,11 @@ # id :integer not null, primary key # project_id :integer # noteable_id :integer -# noteable_type :string(255) +# noteable_type :string # recipient_id :integer -# commit_id :string(255) -# line_code :string(255) -# reply_key :string(255) not null +# commit_id :string +# reply_key :string not null +# line_code :string # class SentNotification < ActiveRecord::Base diff --git a/app/models/service.rb b/app/models/service.rb index 2645b8321d722b0bbfd8e686b7340765a7c5b424..bf16a5453071bf8e2faa17dbc23a14fc6e6b19a6 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -3,12 +3,12 @@ # Table name: services # # id :integer not null, primary key -# type :string(255) -# title :string(255) +# type :string +# title :string # project_id :integer -# created_at :datetime -# updated_at :datetime -# active :boolean default(FALSE), not null +# created_at :datetime not null +# updated_at :datetime not null +# active :boolean not null # properties :text # template :boolean default(FALSE) # push_events :boolean default(TRUE) @@ -17,6 +17,9 @@ # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null # build_events :boolean default(FALSE), not null +# category :string default("common"), not null +# default :boolean default(FALSE) +# wiki_page_events :boolean default(TRUE) # # To add new service you should build a class inherited from Service diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 0fd080619254fc33a06d04fa88eae6e4dd07f0a3..2f905a90942475fd6042b9c69abf200f6949197e 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -3,14 +3,14 @@ # Table name: snippets # # id :integer not null, primary key -# title :string(255) +# title :string # content :text # author_id :integer not null # project_id :integer # created_at :datetime # updated_at :datetime -# file_name :string(255) -# type :string(255) +# file_name :string +# type :string # visibility_level :integer default(0), not null # diff --git a/app/models/spam_log.rb b/app/models/spam_log.rb index 12df68ef83bf17771b16f079216c9b9fe3cd3a97..f49eb7d88e20e3a4ab3d57be4e517d7ada78bfb1 100644 --- a/app/models/spam_log.rb +++ b/app/models/spam_log.rb @@ -1,3 +1,20 @@ +# == Schema Information +# +# Table name: spam_logs +# +# id :integer not null, primary key +# user_id :integer +# source_ip :string +# user_agent :string +# via_api :boolean +# project_id :integer +# noteable_type :string +# title :string +# description :text +# created_at :datetime not null +# updated_at :datetime not null +# + class SpamLog < ActiveRecord::Base belongs_to :user diff --git a/app/models/subscription.rb b/app/models/subscription.rb index dd800ce110fe751a882114417ad888e5c38e5ed5..242faa7d32eb5aa6ba4a40a8fad6eb8d8761bfd6 100644 --- a/app/models/subscription.rb +++ b/app/models/subscription.rb @@ -5,7 +5,7 @@ # id :integer not null, primary key # user_id :integer # subscribable_id :integer -# subscribable_type :string(255) +# subscribable_type :string # subscribed :boolean # created_at :datetime # updated_at :datetime diff --git a/app/models/user.rb b/app/models/user.rb index b6f405c698191f7cc485200580a0b3d84cfe81b6..959b1f93758ffd7f39358c412b02ee919c149cf3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -3,55 +3,55 @@ # Table name: users # # id :integer not null, primary key -# email :string(255) default(""), not null -# encrypted_password :string(255) default(""), not null -# reset_password_token :string(255) +# email :string default(""), not null +# encrypted_password :string default(""), not null +# reset_password_token :string # reset_password_sent_at :datetime # remember_created_at :datetime # sign_in_count :integer default(0) # current_sign_in_at :datetime # last_sign_in_at :datetime -# current_sign_in_ip :string(255) -# last_sign_in_ip :string(255) +# current_sign_in_ip :string +# last_sign_in_ip :string # created_at :datetime # updated_at :datetime -# name :string(255) +# name :string # admin :boolean default(FALSE), not null # projects_limit :integer default(10) -# skype :string(255) default(""), not null -# linkedin :string(255) default(""), not null -# twitter :string(255) default(""), not null -# authentication_token :string(255) +# skype :string default(""), not null +# linkedin :string default(""), not null +# twitter :string default(""), not null +# authentication_token :string # theme_id :integer default(1), not null -# bio :string(255) +# bio :string # failed_attempts :integer default(0) # locked_at :datetime -# username :string(255) +# username :string # can_create_group :boolean default(TRUE), not null # can_create_team :boolean default(TRUE), not null -# state :string(255) +# state :string # color_scheme_id :integer default(1), not null # notification_level :integer default(1), not null # password_expires_at :datetime # created_by_id :integer # last_credential_check_at :datetime -# avatar :string(255) -# confirmation_token :string(255) +# avatar :string +# confirmation_token :string # confirmed_at :datetime # confirmation_sent_at :datetime -# unconfirmed_email :string(255) +# unconfirmed_email :string # hide_no_ssh_key :boolean default(FALSE) -# website_url :string(255) default(""), not null -# notification_email :string(255) +# website_url :string default(""), not null +# notification_email :string # hide_no_password :boolean default(FALSE) # password_automatically_set :boolean default(FALSE) -# location :string(255) -# encrypted_otp_secret :string(255) -# encrypted_otp_secret_iv :string(255) -# encrypted_otp_secret_salt :string(255) +# location :string +# encrypted_otp_secret :string +# encrypted_otp_secret_iv :string +# encrypted_otp_secret_salt :string # otp_required_for_login :boolean default(FALSE), not null # otp_backup_codes :text -# public_email :string(255) default(""), not null +# public_email :string default(""), not null # dashboard :integer default(0) # project_view :integer default(0) # consumed_timestep :integer @@ -59,7 +59,8 @@ # hide_project_limit :boolean default(FALSE) # unlock_token :string # otp_grace_period_started_at :datetime -# external :boolean default(FALSE) +# ldap_email :boolean default(FALSE), not null +# external :boolean default(FALSE) # require 'carrierwave/orm/activerecord' @@ -91,7 +92,7 @@ class User < ActiveRecord::Base devise :two_factor_backupable, otp_number_of_backup_codes: 10 serialize :otp_backup_codes, JSON - devise :lockable, :recoverable, :rememberable, :trackable, + devise :lockable, :async, :recoverable, :rememberable, :trackable, :validatable, :omniauthable, :confirmable, :registerable attr_accessor :force_random_password diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index 707c2f7ff85ffd87372ffab31b7b99656fecd430..9f4481a81538511052883ed090e464300ebcc2fa 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -43,9 +43,4 @@ class CreateBranchService < BaseService out[:branch] = branch out end - - def build_push_data(project, user, branch) - Gitlab::PushDataBuilder. - build(project, user, Gitlab::Git::BLANK_SHA, branch.target, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", []) - end end diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb index 55985380d31793c17b110fa93bdcc48b7a4ba4e5..91ed0e354d05eacfc5749ebb69be272690374570 100644 --- a/app/services/create_tag_service.rb +++ b/app/services/create_tag_service.rb @@ -1,50 +1,30 @@ require_relative 'base_service' class CreateTagService < BaseService - def execute(tag_name, ref, message, release_description = nil) + def execute(tag_name, target, message, release_description = nil) valid_tag = Gitlab::GitRefValidator.validate(tag_name) - if valid_tag == false - return error('Tag name invalid') - end + return error('Tag name invalid') unless valid_tag repository = project.repository - existing_tag = repository.find_tag(tag_name) - if existing_tag - return error('Tag already exists') - end - message.strip! if message - repository.add_tag(tag_name, ref, message) - new_tag = repository.find_tag(tag_name) + new_tag = nil + begin + new_tag = repository.add_tag(current_user, tag_name, target, message) + rescue Rugged::TagError + return error("Tag #{tag_name} already exists") + rescue GitHooksService::PreReceiveError + return error('Tag creation was rejected by Git hook') + end if new_tag - push_data = create_push_data(project, current_user, new_tag) - EventCreateService.new.push(project, current_user, push_data) - project.execute_hooks(push_data.dup, :tag_push_hooks) - project.execute_services(push_data.dup, :tag_push_hooks) - CreateCommitBuildsService.new.execute(project, current_user, push_data) - if release_description CreateReleaseService.new(@project, @current_user). execute(tag_name, release_description) end - - success(new_tag) + success.merge(tag: new_tag) else - error('Invalid reference name') + error("Target #{target} is invalid") end end - - def success(branch) - out = super() - out[:tag] = branch - out - end - - def create_push_data(project, user, tag) - commits = [project.commit(tag.target)].compact - Gitlab::PushDataBuilder. - build(project, user, Gitlab::Git::BLANK_SHA, tag.target, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", commits, tag.message) - end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index b7af80055bf1d38d941f716e2e71ffcee4caf527..66136b6261749df0621bdc4a99409cfb214e04cf 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -17,6 +17,7 @@ class GitPushService < BaseService # 6. Checks if the project's main language has changed # def execute + @project.repository.after_create if @project.empty_repo? @project.repository.after_push_commit(branch_name, params[:newrev]) if push_remove_branch? diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 64271d8bc5c6b7a3ab457b37b58f59d371760cca..7410442609d33fc58d900867fb878876119c8466 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -2,6 +2,7 @@ class GitTagPushService < BaseService attr_accessor :push_data def execute + project.repository.after_create if project.empty_repo? project.repository.before_push_tag @push_data = build_push_data diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 82e7090f1ea52ea318d854c24cace5d40f897921..e61628086f0e8c4ead6a4dd2e17d5dfe5af11141 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -41,14 +41,25 @@ module Issues private def create_new_issue - new_params = { id: nil, iid: nil, label_ids: [], milestone: nil, + new_params = { id: nil, iid: nil, label_ids: cloneable_label_ids, + milestone_id: cloneable_milestone_id, project: @new_project, author: @old_issue.author, description: rewrite_content(@old_issue.description) } - new_params = @old_issue.serializable_hash.merge(new_params) + new_params = @old_issue.serializable_hash.symbolize_keys.merge(new_params) CreateService.new(@new_project, @current_user, new_params).execute end + def cloneable_label_ids + @new_project.labels + .where(title: @old_issue.labels.pluck(:title)).pluck(:id) + end + + def cloneable_milestone_id + @new_project.milestones + .find_by(title: @old_issue.milestone.try(:title)).try(:id) + end + def rewrite_notes @old_issue.notes.find_each do |note| new_note = note.dup diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index fa34753c4fd2d3e4f5b4c2fe0a98d30438a1df9f..cd4230aa5e4d37643e11f73c5c04d31b814bebfe 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 @@ -38,21 +41,45 @@ module MergeRequests merge_request.can_be_created = false end + set_title_and_description(merge_request) + end + + private + + # When your branch name starts with an iid followed by a dash this pattern will be + # interpreted as the user wants to close that issue on this project. + # + # For example: + # - Issue 112 exists, title: Emoji don't show up in commit title + # - Source branch is: 112-fix-mep-mep + # + # Will lead to: + # - Appending `Closes #112` to the description + # - Setting the title as 'Resolves "Emoji don't show up in commit title"' if there is + # more than one commit in the MR + # + def set_title_and_description(merge_request) + if match = merge_request.source_branch.match(/\A(\d+)-/) + iid = match[1] + end + commits = merge_request.compare_commits if commits && commits.count == 1 commit = commits.first merge_request.title = commit.title merge_request.description ||= commit.description.try(:strip) + elsif iid && (issue = merge_request.target_project.get_issue(iid)) && !issue.try(:confidential?) + case issue + when Issue + merge_request.title = "Resolve \"#{issue.title}\"" + when ExternalIssue + merge_request.title = "Resolve #{issue.title}" + end else merge_request.title = merge_request.source_branch.titleize.humanize end - # When your branch name starts with an iid followed by a dash this pattern will - # be interpreted as the use wants to close that issue on this project - # Pattern example: 112-fix-mep-mep - # Will lead to appending `Closes #112` to the description - if match = merge_request.source_branch.match(/\A(\d+)-/) - iid = match[1] + if iid closes_issue = "Closes ##{iid}" if merge_request.description.present? diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index 2bb312bb252ca4096d0b9c22180bca22fcea42e5..01586994813cca5dcca3fe8f628c058d256425dd 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/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index df5054f08d7f35c2d21449ab593cb92a5abb47c0..19aab999e002c4d0d42d7aca271edaaee15e9945 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -7,9 +7,7 @@ module Projects DELETED_FLAG = '+deleted' def pending_delete! - project.update_attribute(:pending_delete, true) - - ProjectDestroyWorker.perform_in(1.minute, project.id, current_user.id, params) + project.schedule_delete!(current_user.id, params) end def execute diff --git a/app/services/wiki_pages/create_service.rb b/app/services/wiki_pages/create_service.rb index 988c663b9d0364461d9cef6fcd9f4ca510ab3a2f..24a817c06c9bfae541f1be57688a46f725f53b65 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/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml index 3571eefd570b543168d524b50b370510c1bb3487..967151bc33be19dc70c2fb8d14f62357735af802 100644 --- a/app/views/admin/builds/_build.html.haml +++ b/app/views/admin/builds/_build.html.haml @@ -35,15 +35,15 @@ %td #{build.stage} / #{build.name} - .pull-right - - if build.tags.any? - - build.tags.each do |tag| - %span.label.label-primary - = tag - - if build.try(:trigger_request) - %span.label.label-info triggered - - if build.try(:allow_failure) - %span.label.label-danger allowed to fail + %td + - if build.tags.any? + - build.tags.each do |tag| + %span.label.label-primary + = tag + - if build.try(:trigger_request) + %span.label.label-info triggered + - if build.try(:allow_failure) + %span.label.label-danger allowed to fail %td.duration - if build.duration @@ -61,12 +61,12 @@ %td .pull-right - if can?(current_user, :read_build, project) && build.artifacts? - = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts' do + = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts', class: 'btn btn-build' do %i.fa.fa-download - if can?(current_user, :update_build, build.project) - if build.active? - = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel' do + = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do %i.fa.fa-remove.cred - elsif defined?(allow_retry) && allow_retry && build.retryable? - = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry' do - %i.fa.fa-repeat + = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do + %i.fa.fa-refresh diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml index 5931efdefe6486e102196715431530ed3fa2dfb7..804d7851bdb09cc369ceaafdc4254cca5e4d3908 100644 --- a/app/views/admin/builds/index.html.haml +++ b/app/views/admin/builds/index.html.haml @@ -19,7 +19,7 @@ - if @all_builds.running_or_pending.any? = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post -.gray-content-block.second-block +.row-content-block.second-block #{(@scope || 'running').capitalize} builds %ul.content-list @@ -38,6 +38,7 @@ %th Ref %th Runner %th Name + %th Tags %th Duration %th Finished at %th diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index 67d23c80233e1c31c1cb4b95b6750aa7f4db4fc2..7b388cf7862dcfeecc0df872943ac156ffe49c40 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/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml index 4b475a4d8fafd2bf96c756a970799a637d31746f..698feb571ac822fcfdcef13bfc632263daa3c1f4 100644 --- a/app/views/admin/logs/show.html.haml +++ b/app/views/admin/logs/show.html.haml @@ -7,7 +7,7 @@ %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') } = link_to klass::file_name, "##{klass::file_name_noext}", 'data-toggle' => 'tab' -.gray-content-block +.row-content-block To prevent performance issues admin logs output the last 2000 lines .tab-content - loggers.each do |klass| diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 0ee8dc962b9dfd28de198ac727dd5328d31c8b59..d6743081c8e29722718c5971dbf89291598e7e48 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -32,7 +32,7 @@ Without projects %small.badge= number_with_delimiter(User.without_projects.count) - .gray-content-block.second-block + .row-content-block.second-block .pull-right .dropdown.inline %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"} diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 49ab8aad1d5e56a248f0a0eb0d067ae9ddc9143e..fc42e5dcc661c84444d18e383d6c95137cbf2556 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -8,14 +8,14 @@ = link_to todos_filter_path(state: 'pending') do %span To do - %span{class: 'badge'} + %span.badge = todos_pending_count - todo_done_active = ('active' if params[:state] == 'done') %li{class: "todos-done #{todo_done_active}"} = link_to todos_filter_path(state: 'done') do %span Done - %span{class: 'badge'} + %span.badge = todos_done_count .nav-controls @@ -25,7 +25,7 @@ = icon('spinner spin') .todos-filters - .gray-content-block.second-block + .row-content-block.second-block = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do .filter-item.inline = select_tag('project_id', todo_projects_options, diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index 22b2c1a186b7c09415bc950fda3bbc9c2fc99c61..c9d1e454a5e174aee6abc9694b67e5e107871d38 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -4,7 +4,7 @@ %h3 Two-factor Authentication .login-body = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| - = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-factor authentication code', required: true, autofocus: true - %p.help-block.hint If you've lost your phone, you may enter one of your recovery codes. + = f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-factor Authentication code', required: true, autofocus: true + %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. .prepend-top-20 = f.submit "Verify code", class: "btn btn-save" diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index e5607dacd0d83b811b862e71f52a252e0c07f1d5..510215bb8cdbc8301720b91eead4f689ba50753b 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -6,7 +6,7 @@ .login-heading %h3 Create an account .login-body - = form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| + = form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name)) do |f| .devise-errors = devise_error_messages! %div @@ -16,7 +16,7 @@ %div = f.email_field :email, class: "form-control middle", placeholder: "Email", required: true .form-group.append-bottom-20#password-strength - = f.password_field :password, class: "form-control bottom", id: "user_password_sign_up", placeholder: "Password", required: true + = f.password_field :password, class: "form-control bottom", placeholder: "Password", required: true %div - if current_application_settings.recaptcha_enabled = recaptcha_tags diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 5d622582088daa60d2dd3542708ac7e873905731..e4629bae0e6fa97a7444d2a0d2bdf05ebd6dcaf4 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -5,7 +5,7 @@ = cache [event, current_application_settings, "v2.2"] do - if event.author - = link_to user_path(event.author.username) do + = link_to user_path(event.author) 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:'' diff --git a/app/views/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml index 5753158c24d30cfff70b5f4405b4b9169cfb5a9f..a1a282178e7dd77b3aa81181a32f7560e5771700 100644 --- a/app/views/events/_event_last_push.html.haml +++ b/app/views/events/_event_last_push.html.haml @@ -1,5 +1,5 @@ - if show_last_push_widget?(event) - .gray-content-block.clear-block.last-push-widget + .row-content-block.clear-block.last-push-widget .event-last-push .event-last-push-text %span You pushed to diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index 8ffca96bb4eee95d644fedc150eee85c6152a830..57f6e7e0612646983d79cab934c96d0f3852efda 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -6,7 +6,7 @@ - else = render 'explore/head' -.gray-content-block.clearfix +.row-content-block.clearfix .pull-left = form_tag explore_groups_path, method: :get, class: 'form-inline form-tiny' do |f| = hidden_field_tag :sort, @sort diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml index 0f100c39ffb8f1c5cd42ad9a0411b5efa456d74a..9b838b9f3b7e1957b0392f638cea8e6428f440b2 100644 --- a/app/views/explore/snippets/index.html.haml +++ b/app/views/explore/snippets/index.html.haml @@ -6,7 +6,7 @@ - else = render 'explore/head' -.gray-content-block +.row-content-block - if current_user .pull-right = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do diff --git a/app/views/groups/activity.html.haml b/app/views/groups/activity.html.haml index f73e1d9e8652fdc398b4a2faefdb7673b9b63dfe..aaad265b3ee316c1a2dcb0b946a1331434e60db8 100644 --- a/app/views/groups/activity.html.haml +++ b/app/views/groups/activity.html.haml @@ -3,7 +3,6 @@ = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity") - page_title "Activity" -- header_title group_title(@group, "Activity", activity_group_path(@group)) %section.activities = render 'activities' diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index a698cbbe9dbd4a5b67ca118cf2fa16b8e1ec8867..92cd4c553d0b4d00072c07866331651381ef1f76 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -1,5 +1,3 @@ -- header_title group_title(@group, "Settings", edit_group_path(@group)) - .panel.panel-default.prepend-top-default .panel-heading Group settings diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 6b7fd5746d6fb1461518985b5e79452c8550b984..0eb6bbd442015e0843a46163d6e6e372cb031cc2 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -1,5 +1,4 @@ - page_title "Members" -- header_title group_title(@group, "Members", group_group_members_path(@group)) .group-members-page.prepend-top-default - if current_user && current_user.can?(:admin_group_member, @group) diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index aea35c50862c5d3da9fa95651aba5c814e2cbdc8..4434f1cbd356388cf31b0701f4253bf1802c7657 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -1,5 +1,4 @@ - page_title "Issues" -- header_title group_title(@group, "Issues", issues_group_path(@group)) = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, issues_group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} issues") @@ -16,7 +15,7 @@ = render 'shared/issuable/filter', type: :issues -.gray-content-block.second-block +.row-content-block.second-block Only issues from %strong #{@group.name} group are listed here. diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index e1c9dd931ee6bfcb0948334a5b1e80b456d7a270..e6953d94531d383598ddacc6a28ef02d12a9cada 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -1,5 +1,4 @@ - page_title "Merge Requests" -- header_title group_title(@group, "Merge Requests", merge_requests_group_path(@group)) .top-area = render 'shared/issuable/nav', type: :merge_requests @@ -8,7 +7,7 @@ = render 'shared/issuable/filter', type: :merge_requests -.gray-content-block.second-block +.row-content-block.second-block Only merge requests from %strong #{@group.name} group are listed here. diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index ab307708b7549c572554e8a7374d054d342c3535..121a7de3ad78647e5bf3323011931bfbd37de388 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -1,5 +1,4 @@ - page_title "Milestones" -- header_title group_title(@group, "Milestones", group_milestones_path(@group)) .top-area = render 'shared/milestones_filter' @@ -10,7 +9,7 @@ = icon('plus') New Milestone -.gray-content-block +.row-content-block Only milestones from %strong #{@group.name} group are listed here. diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index dd75766121ef2f390e06d5db39be7b64e1802a3a..c2f2d9912f78cfbbefc68a174b3803cba98eef38 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -1,5 +1,4 @@ - page_title "Projects" -- header_title group_title(@group, "Projects", projects_group_path(@group)) .panel.panel-default.prepend-top-default .panel-heading diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 3d16ecb097a5735b41970a701c58590343823d8f..77c297255b8367ba727e52307a43a877feb2c33f 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -4,28 +4,20 @@ - if current_user = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity") -.cover-block - .cover-controls - - if @group && can?(current_user, :admin_group, @group) - = link_to icon('pencil'), edit_group_path(@group), class: 'btn' - - if current_user - = link_to icon('rss'), group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'btn rss-btn' - - .avatar-holder +.cover-block.groups-cover-block + .container-fluid.container-limited = link_to group_icon(@group), target: '_blank' do - = image_tag group_icon(@group), class: "avatar group-avatar s90" - .cover-title - %h1 - = @group.name - %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) } - = visibility_level_icon(@group.visibility_level, fw: false) - - .cover-desc.username - @#{@group.path} - - - if @group.description.present? - .cover-desc.description - = markdown(@group.description, pipeline: :description) + = image_tag group_icon(@group), class: "avatar group-avatar s70" + .group-info + .cover-title + %h1 + @#{@group.path} + %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) } + = visibility_level_icon(@group.visibility_level, fw: false) + + - if @group.description.present? + .cover-desc.description + = markdown(@group.description, pipeline: :description) %div{ class: container_class } .top-area diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index f12df5c8ffe282389957d609d109d7e860e5f15d..d676bc28c89dcaa71f298d60bb58278a18287273 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -48,14 +48,14 @@ .lead Gray content block with side padding using - %code .gray-content-block + %code .row-content-block .example - .gray-content-block + .row-content-block %h4 Normal block inside content = lorem - .gray-content-block.second-block + .row-content-block.second-block %h4 Second block = lorem diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index ad8a2e1e6c75622b73d4e3d185ac204702d91ea8..3c3bc41bf0e129da3f3cf8fe80b94f577edd9993 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -26,7 +26,7 @@ .layout-nav .container-fluid = render "layouts/nav/#{nav}" - .content-wrapper + .content-wrapper{ class: ('page-with-layout-nav' if defined?(nav) && nav) } = render "layouts/flash" = yield :flash_message %div{ class: (container_class unless @no_container) } diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml index 2e483b7148d01011f3d4f1070043ef124bfafe9d..f06acc98ca1dbbdeaeeb7141fd6fa12948070428 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -1,6 +1,6 @@ - page_title @group.name - page_description @group.description unless page_description - header_title group_title(@group) unless header_title -- sidebar "group" unless sidebar +- nav "group" = render template: "layouts/application" diff --git a/app/views/layouts/group_settings.html.haml b/app/views/layouts/group_settings.html.haml index a1a1fc2f85847fad8d37efd25c423fd86da81fb8..66b115e36de69e0d332051f3965bfbeb9a3b53fa 100644 --- a/app/views/layouts/group_settings.html.haml +++ b/app/views/layouts/group_settings.html.haml @@ -1,5 +1,4 @@ - page_title "Settings" -- header_title group_title(@group, "Settings", edit_group_path(@group)) -- sidebar "group_settings" +- nav "group" = render template: "layouts/group" diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 3beb8ff7c0daa7a49386a1d14220b1f7d267de08..86930d4eaaf85f67b5b9900e7f4bb4c416985536 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 @@ -23,6 +23,7 @@ = icon('wrench fw') %li = link_to dashboard_todos_path, title: 'Todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('bell fw') %span.badge.todos-pending-count = todos_pending_count - if current_user.can_create_project? diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index ca49c313ff7fddf1dfdce136873b8b624a1b07d1..fad4224e94537970c101bcd3e6a7d7c34739dc9d 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -15,12 +15,12 @@ = icon('dashboard fw') %span Activity - = nav_link(controller: :groups) do + = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do = link_to dashboard_groups_path, title: 'Groups' do = icon('group fw') %span Groups - = nav_link(controller: :milestones) do + = nav_link(controller: 'dashboard/milestones') do = link_to dashboard_milestones_path, title: 'Milestones' do = icon('clock-o fw') %span diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml index f08c5edf99c75c0a4c54b6f0055172906108e019..3b40006a0cce98c536c8bd7e0400880304e3b425 100644 --- a/app/views/layouts/nav/_explore.html.haml +++ b/app/views/layouts/nav/_explore.html.haml @@ -4,7 +4,7 @@ = icon('bookmark fw') %span Projects - = nav_link(controller: :groups) do + = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do = link_to explore_groups_path, title: 'Groups' do = icon('group fw') %span diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 55940741dc07e4ca59c67bd384097e37dc0f50bb..3438005863ad2f3fd729154f62db49ee023ac80f 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -1,12 +1,6 @@ -%ul.nav.nav-sidebar - = nav_link do - = link_to root_path, title: 'Go to dashboard', class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Go to dashboard - - %li.separate-item += render 'layouts/nav/group_settings' +%ul.nav-links = nav_link(path: 'groups#show', html_options: {class: 'home'}) do = link_to group_path(@group), title: 'Home' do = icon('group fw') @@ -28,22 +22,16 @@ %span Issues - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - %span.count= number_with_delimiter(issues.count) + %span.badge.count= number_with_delimiter(issues.count) = nav_link(path: 'groups#merge_requests') do = link_to merge_requests_group_path(@group), title: 'Merge Requests' do = icon('tasks fw') %span Merge Requests - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened').execute - %span.count= number_with_delimiter(merge_requests.count) + %span.badge.count= number_with_delimiter(merge_requests.count) = nav_link(controller: [:group_members]) do = link_to group_group_members_path(@group), title: 'Members' do = icon('users fw') %span Members - - if can?(current_user, :admin_group, @group) - = nav_link(html_options: { class: "separate-item" }) do - = link_to edit_group_path(@group), title: 'Settings' do - = icon ('cogs fw') - %span - Settings diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml index 56a92fe9103b5c5834925db7dd35e7020272fe05..e391ec7f2b7faadcaa379603e3c5a7a0f2b745fe 100644 --- a/app/views/layouts/nav/_group_settings.html.haml +++ b/app/views/layouts/nav/_group_settings.html.haml @@ -1,20 +1,20 @@ -%ul.nav.nav-sidebar - = nav_link do - = link_to group_path(@group), title: 'Go to group', class: 'back-link' do - = icon('caret-square-o-left fw') - %span - Go to group - - %li.separate-item - - %ul.sidebar-subnav - = nav_link(path: 'groups#edit') do - = link_to edit_group_path(@group), title: 'Group Settings' do - = icon ('pencil-square-o fw') - %span - Group Settings - = nav_link(path: 'groups#projects') do - = link_to projects_group_path(@group), title: 'Projects' do - = icon('folder fw') - %span - Projects +- if current_user + - if access = @group.users.find_by(id: current_user.id) + .controls + %span.dropdown.group-settings-dropdown + %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'} + = icon('cog') + = icon('caret-down') + %ul.dropdown-menu.dropdown-menu-align-right + - if can?(current_user, :admin_group, @group) + = nav_link(path: 'groups#projects') do + = link_to projects_group_path(@group), title: 'Projects' do + Projects + %li.divider + %li + = link_to edit_group_path(@group) do + Edit Group + %li + = link_to leave_group_group_members_path(@group), + data: { confirm: leave_group_message(@group.name) }, method: :delete, title: 'Leave group' do + Leave Group 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 0000000000000000000000000000000000000000..2fa2f7846611da52652a8307e29b3992409f1606 --- /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 0000000000000000000000000000000000000000..4d5a406f4b0434a887c229c957bf2f1d96a70e08 --- /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 57527361eb6a6badc1e1fbacae76a9a65a380bf2..6f7fefdb46d5cdeac8f55e5fd32dcf94f41f68a8 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/show.html.haml b/app/views/profiles/show.html.haml index f59d27f7ed013307a7a1c0ff4f786529e4255cc9..eef50d887c7c72053d7fd18217cb1e448f541767 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -8,11 +8,11 @@ %p - if @user.avatar? You can change your avatar here - - if Gitlab.config.gravatar.enabled + - if gravatar_enabled? or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host} - else You can upload an avatar here - - if Gitlab.config.gravatar.enabled + - if gravatar_enabled? or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host} .col-lg-9 .clearfix.avatar-image.append-bottom-default diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml index f0a3e416db7122a564c9fc34103fd0259789198e..7c2b8d015084eab3cce892692e85319cf55d15b2 100644 --- a/app/views/projects/_last_push.html.haml +++ b/app/views/projects/_last_push.html.haml @@ -1,7 +1,7 @@ - if event = last_push_event - if show_last_push_widget?(event) - .gray-content-block.top-block.clear-block.hidden-xs + .row-content-block.top-block.clear-block.hidden-xs .event-last-push .event-last-push-text %span You pushed to diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml index a9908eaeccaf0c5f95806c65e36b57cd2662788e..369a847e7d4974b92daafcf90ed0510f1c4f2645 100644 --- a/app/views/projects/_readme.html.haml +++ b/app/views/projects/_readme.html.haml @@ -7,7 +7,7 @@ = cache(readme_cache_key) do = render_readme(readme) - else - .gray-content-block.second-block.center + .row-content-block.second-block.center %h3.page-title This project does not have a README yet - if can?(current_user, :push_code, @project) diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index 84034c8bf16e952e173235f1b991950014d7e97e..49f95ff37dbe2b323fa1c69476ab3fd865d7eb53 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -1,7 +1,7 @@ - page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds' = render 'projects/builds/header_title' -.top-block.gray-content-block.clearfix +.top-block.row-content-block.clearfix .pull-right = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-default download' do diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index 88266e212301dbd19ff24d5bdeb12d9fc16b4618..ac7790421a406798c58ef1000e0160660fcb475e 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -1,7 +1,7 @@ - page_title "Branches" = render "projects/commits/header_title" = render "projects/commits/head" -.gray-content-block +.row-content-block .pull-right - if can? current_user, :push_code, @project = link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 0406fc21d7799d3ae26116a5b217a6bec1a7f6ad..2e8015d119b66ece4ab1455052eb30b18b713045 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -9,6 +9,7 @@ %span.badge.js-totalbuilds-count = number_with_delimiter(@all_builds.count(:id)) + %li{class: ('active' if @scope == 'running')} = link_to project_builds_path(@project, scope: :running) do Running @@ -34,7 +35,7 @@ = icon('wrench') %span CI Lint -.gray-content-block +.row-content-block #{(@scope || 'running').capitalize} builds from this project %ul.content-list @@ -52,6 +53,7 @@ %th Ref %th Stage %th Name + %th Tags %th Duration %th Finished at - if @project.build_coverage_enabled? diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 99d72aa7935a41303abb6eee7140fbbed1a002bd..c0f7a7686f0b1d7af027e5a88f0e8e7e82e82dd4 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -2,7 +2,7 @@ = render "header_title" .build-page - .gray-content-block.top-block + .row-content-block.top-block Build ##{@build.id} for commit %strong.monospace= link_to @build.commit.short_sha, ci_status_path(@build.commit) from @@ -34,7 +34,7 @@ %i.fa.fa-warning This build was retried. - .gray-content-block.middle-block + .row-content-block.middle-block .build-head .clearfix = ci_status_with_icon(@build.status) diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index e123eb1cc7a339c03a6c675b0e4b5be150e6b81b..8e95f04027384dce8f63274834bd1ad5340985bb 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -40,6 +40,7 @@ %td = build.name + %td .label-container - if build.tags.any? - build.tags.each do |tag| @@ -68,12 +69,12 @@ %td .pull-right - if can?(current_user, :read_build, build) && build.artifacts? - = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts' do + = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts', class: 'btn btn-build' do %i.fa.fa-download - if can?(current_user, :update_build, build) - if build.active? - = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel' do + = link_to cancel_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do %i.fa.fa-remove.cred - elsif defined?(allow_retry) && allow_retry && build.retryable? - = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry' do - %i.fa.fa-repeat + = link_to retry_namespace_project_build_path(build.project.namespace, build.project, build, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do + %i.fa.fa-refresh diff --git a/app/views/projects/commit/_ci_commit.html.haml b/app/views/projects/commit/_ci_commit.html.haml index d3acd33116c88f695363fd86fd6c979ec04d28e4..e849aefb1880cb93f9aaaa16095756ac9dd2fbac 100644 --- a/app/views/projects/commit/_ci_commit.html.haml +++ b/app/views/projects/commit/_ci_commit.html.haml @@ -1,4 +1,4 @@ -.gray-content-block.middle-block +.row-content-block.build-content.middle-block .pull-right - if can?(current_user, :update_build, @project) - if ci_commit.builds.latest.failed.any?(&:retryable?) @@ -40,6 +40,7 @@ %th Build ID %th Stage %th Name + %th Tags %th Duration %th Finished at - if @project.build_coverage_enabled? @@ -49,7 +50,7 @@ = render builds, coverage: @project.build_coverage_enabled?, stage: true, ref: false, allow_retry: true - if ci_commit.retried.any? - .gray-content-block.second-block + .row-content-block.second-block Retried builds .table-holder @@ -61,6 +62,7 @@ %th Ref %th Stage %th Name + %th Tags %th Duration %th Finished at - if @project.build_coverage_enabled? diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 3d7c18a5f58008af7d3886e55e4a2711a58b14bb..01163e526b248fa159b3a59ca7ac503de34a0fc7 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -1,4 +1,4 @@ -.pull-right +.pull-right.commit-action-buttons %div - if @notes_count > 0 %span.btn.disabled.btn-grouped @@ -22,10 +22,12 @@ %div %p - %span.light Commit - = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" - = clipboard_button(clipboard_text: @commit.id) .commit-info-row + - if @commit.status + = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status ci-#{@commit.status}" do + = ci_icon_for_status(@commit.status) + build: + = ci_label_for_status(@commit.status) %span.light Authored by %strong = commit_author_link(@commit, avatar: true, size: 24) @@ -39,19 +41,15 @@ #{time_ago_with_tooltip(@commit.committed_date)} .commit-info-row + %span.light Commit + = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" + = clipboard_button(clipboard_text: @commit.id) %span.cgray= pluralize(@commit.parents.count, "parent") - @commit.parents.each do |parent| = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "monospace" -- if @commit.status - .pull-right - = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status ci-#{@commit.status}" do - = ci_icon_for_status(@commit.status) - build: - = ci_label_for_status(@commit.status) - -.commit-info-row.branches - %i.fa.fa-spinner.fa-spin + %span.commit-info.branches + %i.fa.fa-spinner.fa-spin .commit-box.content-block %h3.commit-title @@ -61,4 +59,4 @@ = preserve(markdown(escape_once(@commit.description), pipeline: :single_line)) :javascript - $(".commit-info-row.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}"); + $(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}"); diff --git a/app/views/projects/commit/branches.html.haml b/app/views/projects/commit/branches.html.haml index 82aac1fbd15e4424b7c7b7b6eeeaa8a366dd4b90..2b0c9a4b4de277374fbf7d3aba6fec4b02515ffe 100644 --- a/app/views/projects/commit/branches.html.haml +++ b/app/views/projects/commit/branches.html.haml @@ -3,7 +3,6 @@ - branch = commit_default_branch(@project, @branches) = link_to(namespace_project_tree_path(@project.namespace, @project, branch)) do %span.label.label-gray - %i.fa.fa-code-fork = branch - if @branches.any? || @tags.any? = link_to("#", class: "js-details-expand") do diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index 7a5b0d993dbd36c958e89536a0590e9ffd15ae88..d1bd76ab5290d593426c160fd4666bdd7fcf6073 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -2,7 +2,8 @@ = nav_link(controller: [:commit, :commits]) do = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do Commits - %span.badge= number_with_delimiter(@repository.commit_count) + %span.badge + = number_with_delimiter(@repository.commit_count) = nav_link(controller: %w(network)) do = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index bcdb09208aa4e3215dc1b0ddaa08a6506f35cf34..088eaa280133141a0f086414f09664ef0066668d 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -6,7 +6,7 @@ = render "head" -.gray-content-block.second-block +.row-content-block.second-block .tree-ref-holder = render 'shared/ref_switcher', destination: 'commits' diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml index 02be5a2d07ffbcca41108b4f9e13278c9f5747a0..5e188dd0f3ca5e0c39a92c3d12ca0b5c78e02242 100644 --- a/app/views/projects/compare/index.html.haml +++ b/app/views/projects/compare/index.html.haml @@ -2,7 +2,7 @@ = render "projects/commits/header_title" = render "projects/commits/head" -.gray-content-block +.row-content-block Compare branches, tags or commit ranges. %br Fill input field with commit id like diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index da731f28bb608109b7b221601e9069a0c4606d39..625251682396de8bdffcbee035317ef7f993883c 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -3,7 +3,7 @@ = render "projects/commits/head" -.gray-content-block +.row-content-block = render "form" - if @commits.present? diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 52d093871b40f8f78ed35ead662940863a6010fa..1a2e59752fe5fab8cef7c1c6223f4728d26e0589 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -7,7 +7,7 @@ = render "home_panel" -.gray-content-block.second-block.center +.row-content-block.second-block.center %h3.page-title The repository for this project is empty - if can?(current_user, :push_code, @project) diff --git a/app/views/projects/graphs/ci.html.haml b/app/views/projects/graphs/ci.html.haml index 6fa77cc10c663d2c160e74ccada3ca980b48ab45..9f05be9982b4324dd5e99410e6bf0806a993b4b0 100644 --- a/app/views/projects/graphs/ci.html.haml +++ b/app/views/projects/graphs/ci.html.haml @@ -1,7 +1,7 @@ - page_title "Continuous Integration", "Graphs" = render "header_title" = render 'head' -.gray-content-block.append-bottom-default +.row-content-block.append-bottom-default .oneline A collection of graphs for Continuous Integration diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml index fc465ab273bd96bb1d8a838af53755546f470b5d..da9f648cc9c50430a507a9da1ba097ed2e16fabb 100644 --- a/app/views/projects/graphs/commits.html.haml +++ b/app/views/projects/graphs/commits.html.haml @@ -2,7 +2,7 @@ = render "header_title" = render 'head' -.gray-content-block.append-bottom-default +.row-content-block.append-bottom-default .tree-ref-holder = render 'shared/ref_switcher', destination: 'graphs_commits' %ul.breadcrumb.repo-breadcrumb diff --git a/app/views/projects/graphs/languages.html.haml b/app/views/projects/graphs/languages.html.haml index a7fab5b6d72b3122f288352fd5a78f6baa7ed398..ebecab1dbfcdf509133f8ce1767879fdb9572354 100644 --- a/app/views/projects/graphs/languages.html.haml +++ b/app/views/projects/graphs/languages.html.haml @@ -2,7 +2,7 @@ = render "header_title" = render 'head' -.gray-content-block.append-bottom-default +.row-content-block.append-bottom-default .oneline Programming languages used in this repository diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index 882e7d6b6ee42c320cd529563af8a845d15215e4..ad4a932d391fb8212fdc16688346266ac8a24855 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -2,7 +2,7 @@ = render "header_title" = render 'head' -.gray-content-block.append-bottom-default +.row-content-block.append-bottom-default .tree-ref-holder = render 'shared/ref_switcher', destination: 'graphs' %ul.breadcrumb.repo-breadcrumb diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml index 6f1ee209430d9f7a68e6c1a7951e416d966075ff..36c1d69f060d9d5cf9f72fd66aed2eaacb4664c3 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -15,6 +15,11 @@ .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 diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml index 6027fb23360ffb3904e81f42112442b0438ec6fb..a8a8caf728036728c601804cad75f5a6e5f9b80f 100644 --- a/app/views/projects/imports/new.html.haml +++ b/app/views/projects/imports/new.html.haml @@ -10,7 +10,7 @@ .panel-body %pre :preserve - #{@project.import_error.try(:strip)} + #{sanitize_repo_path(@project.import_error)} = form_for @project, url: namespace_project_import_path(@project.namespace, @project), method: :post, html: { class: 'form-horizontal' } do |f| = render "shared/import_form", f: f diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 56543ccd0625a65b71a0a523879dad9b2709c440..6ec8466015760617b20e1a35db0adf9bd527a022 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -24,15 +24,15 @@ - else = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped" - = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-nr" do - = icon('trash-o') - Delete - = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped btn-nr" do = icon('pencil-square-o') Edit -.detail-page-description.milestone-detail.second-block + = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do + = icon('trash-o') + Delete + +.detail-page-description.milestone-detail %h2.title = markdown escape_once(@milestone.title), pipeline: :single_line %div diff --git a/app/views/projects/network/_head.html.haml b/app/views/projects/network/_head.html.haml index 28a617538b50e2ff6e5412f4c49e20da662fac87..c609c505def0213531d37ab884996b79a32a91d9 100644 --- a/app/views/projects/network/_head.html.haml +++ b/app/views/projects/network/_head.html.haml @@ -1,4 +1,4 @@ -.gray-content-block.append-bottom-default +.row-content-block.append-bottom-default .tree-ref-holder = render partial: 'shared/ref_switcher', locals: {destination: 'graph'} diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index 6f0b32aa165a16310411db197ca826802da168cb..0d59cec322ca9b966df27de658e7882c4defdd34 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -2,7 +2,7 @@ = render "projects/commits/header_title" = render "projects/commits/head" -.gray-content-block +.row-content-block .oneline .title Release notes for tag diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index d854ac217256b8c042e1abda2793f34d8f1455e7..74feb9e3282db65f5abbd1be9610a01199e53a94 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -12,7 +12,7 @@ = render 'projects/last_push' = render "home_panel" -.project-stats.gray-content-block.second-block +.project-stats.row-content-block.second-block %ul.nav %li = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index 4af963e14da55f6ad7952dbebfc02f571309b180..103ff447464e242a9d1292b15ef4b2753edf3030 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -1,7 +1,7 @@ - page_title "Snippets" = render "header_title" -.gray-content-block.top-block +.row-content-block.top-block .pull-right = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New Snippet" do = icon('plus') diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 760347de0a9943a853c4e23770b3a3e305580779..dc6ece30dd292eb551f96e66f83ece6d42bd5141 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -2,7 +2,7 @@ = render "projects/commits/header_title" = render "projects/commits/head" -.gray-content-block +.row-content-block - if can? current_user, :push_code, @project .pull-right = link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 1dc9b799a95db2f5a925ffb96a5fb17bd09af890..9c916fd02de7b53679dadeccee4135d457a67be7 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -2,7 +2,7 @@ = render "projects/commits/header_title" = render "projects/commits/head" -.gray-content-block +.row-content-block .pull-right - if can?(current_user, :push_code, @project) = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn-grouped btn has-tooltip', title: 'Edit release notes' do diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index dd27ea2b11bc6aad8b1e910e39def9b47b954967..ba3f2cadc4811ca0c129ceb2d5ce995b64da2730 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -2,7 +2,7 @@ = render "header_title" = render 'nav' -.gray-content-block +.row-content-block %span.oneline Git access for %strong= @project_wiki.path_with_namespace diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index 711337f308e78667fe502de141498127bbc7acc0..252c37532e16370d1f4cd06145947ff2627c5772 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -1,7 +1,7 @@ - if @search_objects.empty? = render partial: "search/results/empty" - else - .gray-content-block + .row-content-block = search_entries_info(@search_objects, @scope, @search_term) - unless @show_snippets - if @project diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml index 34241cd8aad353d83a570e2b73a858164586ac15..b0fc60573f734abc6424b08b1731ac5b7186deee 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/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index fc1101646fb6ea37b019964f2a069cc96580af28..9474462cbd1195de18dfe9c595938dfe2b288b66 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -1,5 +1,5 @@ .issues-filters - .issues-details-filters.gray-content-block.second-block + .issues-details-filters.row-content-block.second-block = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name]), method: :get, class: 'filter-form' do - if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project) .check-all-holder @@ -48,7 +48,7 @@ = button_tag "Update issues", class: "btn update_selected_issues btn-save" - if !@labels.nil? - .gray-content-block.second-block.filtered-labels{ class: ("hidden" if !@labels.any?) } + .row-content-block.second-block.filtered-labels{ class: ("hidden" if !@labels.any?) } - if @labels.any? = render "shared/labels_row", labels: @labels diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 18b091df39ba611d3afbcfe85bfb97340942c795..5c52cc6d1daed5ec4d882ff1daeb98bccc2e9f78 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -116,7 +116,7 @@ = link_to 'Change branches', mr_change_branches_path(@merge_request) - is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?) -.gray-content-block{class: (is_footer ? "footer-block" : "middle-block")} +.row-content-block{class: (is_footer ? "footer-block" : "middle-block")} - if issuable.new_record? = f.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create' - else diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml index cab8743a0772dca5e169fe3d4b3fbc16ab993154..7ff947a51db8ad8a13caa9fb055a8bb9b40f2581 100644 --- a/app/views/shared/milestones/_top.html.haml +++ b/app/views/shared/milestones/_top.html.haml @@ -24,7 +24,7 @@ - else = link_to 'Reopen Milestone', group_milestone_path(group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" -.detail-page-description.gray-content-block.second-block +.detail-page-description.milestone-detail %h2.title = markdown escape_once(milestone.title), pipeline: :single_line diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index 3c445f672368b13c54cebcb9f6f7c1703dd9a893..e65b181487238d529f5b8e5196d0cbfdd83c16a1 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -20,6 +20,6 @@ - else = render "snippets/actions" -.detail-page-description.gray-content-block.second-block +.detail-page-description.row-content-block.second-block %h2.title = markdown escape_once(@snippet.title), pipeline: :single_line diff --git a/app/views/sherlock/file_samples/show.html.haml b/app/views/sherlock/file_samples/show.html.haml index cfd11e45b6af80d5721f73379ea6074498d7136e..94d4dd4fa7d78f277c2fe75666e399f35ef8b400 100644 --- a/app/views/sherlock/file_samples/show.html.haml +++ b/app/views/sherlock/file_samples/show.html.haml @@ -3,7 +3,7 @@ - header_title t('sherlock.title'), sherlock_transactions_path -.gray-content-block +.row-content-block .pull-right = link_to(sherlock_transaction_path(@transaction), class: 'btn') do %i.fa.fa-arrow-left diff --git a/app/views/sherlock/queries/show.html.haml b/app/views/sherlock/queries/show.html.haml index 83f61ce4b073d2b2ca73c4befbea598688cc9042..fc2863dca8e065db325d55c2909b924d73f5ff64 100644 --- a/app/views/sherlock/queries/show.html.haml +++ b/app/views/sherlock/queries/show.html.haml @@ -9,7 +9,7 @@ %a(href="#tab-backtrace" data-toggle="tab") = t('sherlock.backtrace') -.gray-content-block +.row-content-block .pull-right = link_to(sherlock_transaction_path(@transaction), class: 'btn') do %i.fa.fa-arrow-left diff --git a/app/views/sherlock/transactions/index.html.haml b/app/views/sherlock/transactions/index.html.haml index 010e1a2a9023e8d5a5d96fc63f67c0963cfebd4a..da969c0276523275b7c8457ee65f7596a6eb809d 100644 --- a/app/views/sherlock/transactions/index.html.haml +++ b/app/views/sherlock/transactions/index.html.haml @@ -1,7 +1,7 @@ - page_title t('sherlock.title') - header_title t('sherlock.title'), sherlock_transactions_path -.gray-content-block +.row-content-block .pull-right = link_to(destroy_all_sherlock_transactions_path, class: 'btn btn-danger', diff --git a/app/views/sherlock/transactions/show.html.haml b/app/views/sherlock/transactions/show.html.haml index 9d4b0b2724c2c3be77b7d7aa29ecb88fd0284df2..8aa6b437d953263757e6e0cd7450538ecbb25d73 100644 --- a/app/views/sherlock/transactions/show.html.haml +++ b/app/views/sherlock/transactions/show.html.haml @@ -16,7 +16,7 @@ %span.badge #{@transaction.file_samples.length} -.gray-content-block +.row-content-block .pull-right = link_to(sherlock_transactions_path, class: 'btn') do %i.fa.fa-arrow-left diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 0dff27f9654aa0c483d3395cd0a8bc8f842eb8e8..3c0b89c67416a4f1e65bbe3adbb1c832a830606c 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -85,7 +85,7 @@ %div{ class: container_class } .tab-content #activity.tab-pane - .gray-content-block.calender-block.white.second-block.hidden-xs + .row-content-block.calender-block.white.second-block.hidden-xs %div{ class: container_class } .user-calendar{data: {href: user_calendar_path}} %h4.center.light @@ -98,7 +98,7 @@ #groups.tab-pane - # This tab is always loaded via AJAX - #contributed.contributed-projects.tab-pane + #contributed.tab-pane - # This tab is always loaded via AJAX #projects.tab-pane diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml index 59e127986917e3438645163db92f0354c52d2dc7..4beb87464444868a114e513741444a9fa8049aa0 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 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, + window.awardsHandler = new AwardsHandler( + getEmojisUrl, + postEmojiUrl, + noteableType, + noteableId, unicodes ); diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb index a76729e3c74ead9b2a8e5fbc434772876297b602..f2d12ba5a7d147dec20a206db34c1bedd822998e 100644 --- a/app/workers/repository_check/single_repository_worker.rb +++ b/app/workers/repository_check/single_repository_worker.rb @@ -1,9 +1,9 @@ module RepositoryCheck class SingleRepositoryWorker include Sidekiq::Worker - + sidekiq_options retry: false - + def perform(project_id) project = Project.find(project_id) project.update_columns( @@ -11,20 +11,32 @@ module RepositoryCheck last_repository_check_at: Time.now, ) end - + private - + def check(project) - repositories = [project.repository] - repositories << project.wiki.repository if project.wiki_enabled? - # Use 'map do', not 'all? do', to prevent short-circuiting - repositories.map { |repository| git_fsck(repository.path_to_repo) }.all? + if !git_fsck(project.repository) + false + elsif project.wiki_enabled? + # Historically some projects never had their wiki repos initialized; + # this happens on project creation now. Let's initialize an empty repo + # if it is not already there. + begin + project.create_wiki + rescue Rugged::RepositoryError + end + + git_fsck(project.wiki.repository) + else + true + end end - - def git_fsck(path) + + def git_fsck(repository) + path = repository.path_to_repo cmd = %W(nice git --git-dir=#{path} fsck) output, status = Gitlab::Popen.popen(cmd) - + if status.zero? true else diff --git a/bin/background_jobs b/bin/background_jobs index 1f67d73294965bfee3252f968c20b4d271b3b7b8..25a578a1c491609b73832f0d9c3ee4c5a35e2081 100755 --- a/bin/background_jobs +++ b/bin/background_jobs @@ -37,7 +37,7 @@ start_no_deamonize() start_sidekiq() { - bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile "$@" + exec bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile "$@" } load_ok() diff --git a/bin/web b/bin/web index 03fe7a6354b6136cd7d5363d00a6056dae9d8ce1..ecd0bbd10b04d597efc873ed1dc3060a0f48d8aa 100755 --- a/bin/web +++ b/bin/web @@ -19,12 +19,12 @@ get_unicorn_pid() start() { - $unicorn_cmd -D + exec $unicorn_cmd -D } start_foreground() { - $unicorn_cmd + exec $unicorn_cmd } stop() diff --git a/config/application.rb b/config/application.rb index abe22691ad1234026b34d01fbac54c24bd3ee28c..d4b86bb38bb189dd230ac13b465c4d265c811154 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/gitlab.yml.example b/config/gitlab.yml.example index e55ca6f9c6bab699ca519f50e9439b6be21cdaa7..cbb7c656fe7a757075aee777aec3012d88e3ebbb 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -152,7 +152,6 @@ production: &base ## Gravatar ## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html gravatar: - enabled: true # Use user avatar image from Gravatar.com (default: true) # gravatar urls: possible placeholders: %{hash} %{size} %{email} # plain_url: "http://..." # default: http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon # ssl_url: "https://..." # default: https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon diff --git a/config/initializers/devise_async.rb b/config/initializers/devise_async.rb new file mode 100644 index 0000000000000000000000000000000000000000..05a1852cdbd9d141a757755983adc59ad467f3ca --- /dev/null +++ b/config/initializers/devise_async.rb @@ -0,0 +1 @@ +Devise::Async.backend = :sidekiq diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb index 283936d0efc7937b4848fb7e024b3d08320a4006..b2d08d87bacb6c246429592319adb50d8c1c9389 100644 --- a/config/initializers/metrics.rb +++ b/config/initializers/metrics.rb @@ -61,12 +61,30 @@ if Gitlab::Metrics.enabled? config.instrument_instance_methods(const) end - Dir[Rails.root.join('app', 'finders', '*.rb')].each do |path| - const = File.basename(path, '.rb').camelize.constantize - - config.instrument_instance_methods(const) + # Path to search => prefix to strip from constant + paths_to_instrument = { + ['app', 'finders'] => ['app', 'finders'], + ['app', 'mailers', 'emails'] => ['app', 'mailers'], + ['app', 'services', '**'] => ['app', 'services'], + ['lib', 'gitlab', 'diff'] => ['lib'], + ['lib', 'gitlab', 'email', 'message'] => ['lib'] + } + + paths_to_instrument.each do |(path, prefix)| + prefix = Rails.root.join(*prefix) + + Dir[Rails.root.join(*path + ['*.rb'])].each do |file_path| + path = Pathname.new(file_path).relative_path_from(prefix) + const = path.to_s.sub('.rb', '').camelize.constantize + + config.instrument_methods(const) + config.instrument_instance_methods(const) + end end + config.instrument_methods(Premailer::Adapter::Nokogiri) + config.instrument_instance_methods(Premailer::Adapter::Nokogiri) + [ :Blame, :Branch, :BranchCollection, :Blob, :Commit, :Diff, :Repository, :Tag, :TagCollection, :Tree @@ -97,17 +115,6 @@ if Gitlab::Metrics.enabled? config.instrument_methods(Gitlab::ReferenceExtractor) config.instrument_instance_methods(Gitlab::ReferenceExtractor) - # Instrument all service classes - services = Rails.root.join('app', 'services') - - Dir[services.join('**', '*.rb')].each do |file_path| - path = Pathname.new(file_path).relative_path_from(services) - const = path.to_s.sub('.rb', '').camelize.constantize - - config.instrument_methods(const) - config.instrument_instance_methods(const) - end - # Instrument the classes used for checking if somebody has push access. config.instrument_instance_methods(Gitlab::GitAccess) config.instrument_instance_methods(Gitlab::GitAccessWiki) diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index e87899b2d5c59628a3b42d6fe9d0415bbc459678..74fef7cadfe24904b5d1425ce7506df5d95856d8 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 88cb859871c31bff0c3b7519b1cf9b011046467c..599dabb9e503433877f90747510511fd787c7cca 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 5b48819dd9d18adb20e88d56fe39d38cd6f2cdcb..bfc6818a8d89c8ad44094d9c95419618cb79b3a2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -216,8 +216,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 @@ -227,12 +225,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] diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb index 0825776ffaad1693f84597d14446f2028b201026..87fb8e3300d735a7e0f90c525c79fa0cfc174780 100644 --- a/db/fixtures/development/10_merge_requests.rb +++ b/db/fixtures/development/10_merge_requests.rb @@ -1,6 +1,9 @@ Gitlab::Seeder.quiet do + # Limit the number of merge requests per project to avoid long seeds + MAX_NUM_MERGE_REQUESTS = 10 + Project.all.reject(&:empty_repo?).each do |project| - branches = project.repository.branch_names + branches = project.repository.branch_names.sample(MAX_NUM_MERGE_REQUESTS * 2) branches.each do |branch_name| break if branches.size < 2 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 0000000000000000000000000000000000000000..f04225068cd2cf08c851b0d062d66b1f6450d19a --- /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 bf46028d23f9e4319ec4cd44f730addf46249385..7ea16e21358065dc9c8a227c4cb6bed183c54a34 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -70,16 +70,16 @@ ActiveRecord::Schema.define(version: 20160421130527) do t.string "recaptcha_site_key" t.string "recaptcha_private_key" t.integer "metrics_port", default: 8089 + t.boolean "akismet_enabled", default: false + t.string "akismet_api_key" t.integer "metrics_sample_interval", default: 15 t.boolean "sentry_enabled", default: false t.string "sentry_dsn" - t.boolean "akismet_enabled", default: false - t.string "akismet_api_key" t.boolean "email_author_in_body", default: false t.integer "default_group_visibility" t.boolean "repository_checks_enabled", default: false - t.integer "metrics_packet_size", default: 1 t.text "shared_runners_text" + t.integer "metrics_packet_size", default: 1 end create_table "audit_events", force: :cascade do |t| @@ -426,10 +426,10 @@ ActiveRecord::Schema.define(version: 20160421130527) do t.string "state" t.integer "iid" t.integer "updated_by_id" - t.integer "moved_to_id" t.boolean "confidential", default: false t.datetime "deleted_at" t.date "due_date" + t.integer "moved_to_id" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -716,8 +716,8 @@ ActiveRecord::Schema.define(version: 20160421130527) do t.integer "project_id" t.text "data" t.text "encrypted_credentials" - t.text "encrypted_credentials_iv" - t.text "encrypted_credentials_salt" + t.string "encrypted_credentials_iv" + t.string "encrypted_credentials_salt" end create_table "projects", force: :cascade do |t| @@ -817,9 +817,9 @@ ActiveRecord::Schema.define(version: 20160421130527) do t.string "type" t.string "title" t.integer "project_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.boolean "active", null: false + t.datetime "created_at" + t.datetime "updated_at" + t.boolean "active", default: false, null: false t.text "properties" t.boolean "template", default: false t.boolean "push_events", default: true @@ -1026,6 +1026,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/api/commits.md b/doc/api/commits.md index 6341440c58b9017eab77981d9cdc3e8f5d04f45e..57c2e1d9b8710de15587cea2921a5b01a8a1bec4 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -12,6 +12,8 @@ GET /projects/:id/repository/commits | --------- | ---- | -------- | ----------- | | `id` | integer | yes | The ID of a project | | `ref_name` | string | no | The name of a repository branch or tag or if not given the default branch | +| `since` | string | no | Only commits after or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | +| `until` | string | no | Only commits before or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | ```bash curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits" diff --git a/doc/api/notes.md b/doc/api/notes.md index 7aa1c2155bfe8d5c2c2b5f30ffd0002ac71115a4..a6b5b1787fdf90550afb4e934c82773a7220be57 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -15,7 +15,7 @@ GET /projects/:id/issues/:issue_id/notes Parameters: - `id` (required) - The ID of a project -- `issue_id` (required) - The ID of an issue +- `issue_id` (required) - The IID of an issue (not ID) ```json [ @@ -73,7 +73,7 @@ GET /projects/:id/issues/:issue_id/notes/:note_id Parameters: - `id` (required) - The ID of a project -- `issue_id` (required) - The ID of a project issue +- `issue_id` (required) - The IID of a project issue (not ID) - `note_id` (required) - The ID of an issue note ### Create new issue note @@ -87,7 +87,7 @@ POST /projects/:id/issues/:issue_id/notes Parameters: - `id` (required) - The ID of a project -- `issue_id` (required) - The ID of an issue +- `issue_id` (required) - The IID of an issue (not ID) - `body` (required) - The content of a note - `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z @@ -102,7 +102,7 @@ PUT /projects/:id/issues/:issue_id/notes/:note_id Parameters: - `id` (required) - The ID of a project -- `issue_id` (required) - The ID of an issue +- `issue_id` (required) - The IID of an issue (not ID) - `note_id` (required) - The ID of a note - `body` (required) - The content of a note @@ -120,7 +120,7 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer | yes | The ID of a project | -| `issue_id` | integer | yes | The ID of an issue | +| `issue_id` | integer | yes | The IID of an issue | | `note_id` | integer | yes | The ID of a note | ```bash diff --git a/doc/api/users.md b/doc/api/users.md index 7d2b4897cffd215cc5b2a876eea8640eb6608e61..7e848586dbd02e11c2b3da7b716e4c249ebc9954 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -20,6 +20,7 @@ GET /users "name": "John Smith", "state": "active", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg", + "web_url": "http://localhost:3000/u/john_smith" }, { "id": 2, @@ -27,6 +28,7 @@ GET /users "name": "Jack Smith", "state": "blocked", "avatar_url": "http://gravatar.com/../e32131cd8.jpeg", + "web_url": "http://localhost:3000/u/jack_smith" } ] ``` @@ -45,21 +47,31 @@ GET /users "email": "john@example.com", "name": "John Smith", "state": "active", + "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", + "web_url": "http://localhost:3000/u/john_smith", "created_at": "2012-05-23T08:00:58Z", + "is_admin": false, "bio": null, + "location": null, "skype": "", "linkedin": "", "twitter": "", "website_url": "", - "extern_uid": "john.smith", - "provider": "provider_name", + "last_sign_in_at": "2012-06-01T11:41:01Z", + "confirmed_at": "2012-05-23T09:05:22Z", "theme_id": 1, "color_scheme_id": 2, - "is_admin": false, - "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg", + "projects_limit": 100, + "current_sign_in_at": "2012-06-02T06:36:55Z", + "identities": [ + {"provider": "github", "extern_uid": "2435223452345"}, + {"provider": "bitbucket", "extern_uid": "john.smith"}, + {"provider": "google_oauth2", "extern_uid": "8776128412476123468721346"} + ], "can_create_group": true, - "current_sign_in_at": "2014-03-19T13:12:15Z", - "two_factor_enabled": true + "can_create_project": true, + "two_factor_enabled": true, + "external": false }, { "id": 2, @@ -67,24 +79,27 @@ GET /users "email": "jack@example.com", "name": "Jack Smith", "state": "blocked", + "avatar_url": "http://localhost:3000/uploads/user/avatar/2/index.jpg", + "web_url": "http://localhost:3000/u/jack_smith", "created_at": "2012-05-23T08:01:01Z", + "is_admin": false, "bio": null, "location": null, "skype": "", "linkedin": "", "twitter": "", "website_url": "", - "extern_uid": "jack.smith", - "provider": "provider_name", + "last_sign_in_at": null, + "confirmed_at": "2012-05-30T16:53:06.148Z", "theme_id": 1, "color_scheme_id": 3, - "is_admin": false, - "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg", - "can_create_group": true, - "can_create_project": true, "projects_limit": 100, "current_sign_in_at": "2014-03-19T17:54:13Z", - "two_factor_enabled": false + "identities": [], + "can_create_group": true, + "can_create_project": true, + "two_factor_enabled": true, + "external": false } ] ``` @@ -124,6 +139,7 @@ Parameters: "name": "John Smith", "state": "active", "avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg", + "web_url": "http://localhost:3000/u/john_smith", "created_at": "2012-05-23T08:00:58Z", "is_admin": false, "bio": null, @@ -152,23 +168,31 @@ Parameters: "email": "john@example.com", "name": "John Smith", "state": "active", + "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", + "web_url": "http://localhost:3000/u/john_smith", "created_at": "2012-05-23T08:00:58Z", - "confirmed_at": "2012-05-23T08:00:58Z", - "last_sign_in_at": "2015-03-23T08:00:58Z", + "is_admin": false, "bio": null, "location": null, "skype": "", "linkedin": "", "twitter": "", "website_url": "", - "extern_uid": "john.smith", - "provider": "provider_name", + "last_sign_in_at": "2012-06-01T11:41:01Z", + "confirmed_at": "2012-05-23T09:05:22Z", "theme_id": 1, "color_scheme_id": 2, - "is_admin": false, + "projects_limit": 100, + "current_sign_in_at": "2012-06-02T06:36:55Z", + "identities": [ + {"provider": "github", "extern_uid": "2435223452345"}, + {"provider": "bitbucket", "extern_uid": "john.smith"}, + {"provider": "google_oauth2", "extern_uid": "8776128412476123468721346"} + ], "can_create_group": true, "can_create_project": true, - "projects_limit": 100 + "two_factor_enabled": true, + "external": false } ``` @@ -261,21 +285,33 @@ GET /user "username": "john_smith", "email": "john@example.com", "name": "John Smith", - "private_token": "dd34asd13as", "state": "active", + "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", + "web_url": "http://localhost:3000/u/john_smith", "created_at": "2012-05-23T08:00:58Z", + "is_admin": false, "bio": null, "location": null, "skype": "", "linkedin": "", "twitter": "", "website_url": "", + "last_sign_in_at": "2012-06-01T11:41:01Z", + "confirmed_at": "2012-05-23T09:05:22Z", "theme_id": 1, "color_scheme_id": 2, - "is_admin": false, + "projects_limit": 100, + "current_sign_in_at": "2012-06-02T06:36:55Z", + "identities": [ + {"provider": "github", "extern_uid": "2435223452345"}, + {"provider": "bitbucket", "extern_uid": "john_smith"}, + {"provider": "google_oauth2", "extern_uid": "8776128412476123468721346"} + ], "can_create_group": true, "can_create_project": true, - "projects_limit": 100 + "two_factor_enabled": true, + "external": false, + "private_token": "dd34asd13as" } ``` diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index 5fb086b1dd95ab63273ffb76a3fc83edd65d32ae..ca52a483a593ef7c4e6f72ab5d849470b62f9bf0 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -41,9 +41,9 @@ GitLab Runner then executes build scripts as `gitlab-runner` user. --description "My Runner" ``` -2. Install Docker on server. +2. Install Docker Engine on server. - For more information how to install Docker on different systems checkout the [Supported installations](https://docs.docker.com/installation/). + For more information how to install Docker Engine on different systems checkout the [Supported installations](https://docs.docker.com/engine/installation/). 3. Add `gitlab-runner` user to `docker` group: @@ -151,4 +151,4 @@ In order to do that follow the steps: 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 +[docker-cap]: https://docs.docker.com/engine/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 84212fb3c617b67a6606667b0e4e88ee1c301cc5..56ac2195c49cc4a39b52913ccae514d3884f9d30 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -64,7 +64,7 @@ You can see some widely used services examples in the relevant documentation of ### How is service linked to the build To better understand how the container linking works, read -[Linking containers together](https://docs.docker.com/userguide/dockerlinks/). +[Linking containers together][linking-containers]. To summarize, if you add `mysql` as service to your application, the image will then be used to create a container that is linked to the build container. @@ -273,7 +273,7 @@ creation. [Docker Fundamentals]: https://docs.docker.com/engine/understanding-docker/ [hub]: https://hub.docker.com/ [linking-containers]: https://docs.docker.com/engine/userguide/networking/default_network/dockerlinks/ -[tutum/wordpress]: https://registry.hub.docker.com/u/tutum/wordpress/ -[postgres-hub]: https://registry.hub.docker.com/u/library/postgres/ -[mysql-hub]: https://registry.hub.docker.com/u/library/mysql/ +[tutum/wordpress]: https://hub.docker.com/r/tutum/wordpress/ +[postgres-hub]: https://hub.docker.com/r/_/postgres/ +[mysql-hub]: https://hub.docker.com/r/_/mysql/ [runner-priv-reg]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#using-a-private-docker-registry diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index cc059dc437633fd93fff4e4228ea15c6eef8f540..61294be599d3a8a411fe23c036834f037bdfd959 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -4,12 +4,12 @@ - [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md) - [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md) - [Test a Clojure application](test-clojure-application.md) -- [Using `dpl` as deployment tool](deployment/README.md) +- [Using `dpl` as deployment tool](../deployment/README.md) - Help your favorite programming language and GitLab by sending a merge request with a guide for that language. ## Outside the documentation -- [Blost post about using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/) +- [Blog post about using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/) - [Repo's with examples for various languages](https://gitlab.com/groups/gitlab-examples) - [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml) diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md index db07792712670ea149213059cb9741ba18675684..2695301450245de9155cf1e4242f59ec2802d35e 100644 --- a/doc/ci/examples/php.md +++ b/doc/ci/examples/php.md @@ -60,7 +60,7 @@ docker-php-ext-install pdo_mysql You might wonder what `docker-php-ext-install` is. In short, it is a script provided by the official php docker image that you can use to easilly install extensions. For more information read the the documentation at -<https://hub.docker.com/_/php/>. +<https://hub.docker.com/r/_/php/>. Now that we created the script that contains all prerequisites for our build environment, let's add it in `.gitlab-ci.yml`: @@ -92,7 +92,7 @@ Finally, commit your files and push them to GitLab to see your build succeeding The final `.gitlab-ci.yml` should look similar to this: ```yaml -# Select image from https://hub.docker.com/_/php/ +# Select image from https://hub.docker.com/r/_/php/ image: php:5.6 before_script: @@ -278,7 +278,7 @@ that runs on [GitLab.com](https://gitlab.com) using our publicly available Want to hack on it? Simply fork it, commit and push your changes. Within a few moments the changes will be picked by a public runner and the build will begin. -[php-hub]: https://hub.docker.com/_/php/ +[php-hub]: https://hub.docker.com/r/_/php/ [phpenv]: https://github.com/phpenv/phpenv [phpenv-installation]: https://github.com/phpenv/phpenv#installation [php-example-repo]: https://gitlab.com/gitlab-examples/php diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md index a236da53fe9e03907d694f05f90c4913228f4430..e4d3970deac8afc8691f4fbfcb085e53794618aa 100644 --- a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md +++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md @@ -8,7 +8,7 @@ This is what the `.gitlab-ci.yml` file looks like for this project: ```yaml test: script: - # this configures django application to use attached postgres database that is run on `postgres` host + # this configures Django application to use attached postgres database that is run on `postgres` host - export DATABASE_URL=postgres://postgres:@postgres:5432/python-test-app - apt-get update -qy - apt-get install -y python-dev python-pip @@ -37,7 +37,7 @@ production: ``` This project has three jobs: -1. `test` - used to test rails application, +1. `test` - used to test Django application, 2. `staging` - used to automatically deploy staging environment every push to `master` branch 3. `production` - used to automatically deploy production environmnet for every created tag @@ -61,12 +61,12 @@ gitlab-ci-multi-runner register \ --non-interactive \ --url "https://gitlab.com/ci/" \ --registration-token "PROJECT_REGISTRATION_TOKEN" \ - --description "python-3.2" \ + --description "python-3.5" \ --executor "docker" \ - --docker-image python:3.2 \ + --docker-image python:3.5 \ --docker-postgres latest ``` -With the command above, you create a runner that uses [python:3.2](https://registry.hub.docker.com/u/library/python/) image and uses [postgres](https://registry.hub.docker.com/u/library/postgres/) database. +With the command above, you create a runner that uses [python:3.5](https://hub.docker.com/r/_/python/) image and uses [postgres](https://hub.docker.com/r/_/postgres/) database. To access PostgreSQL database you need to connect to `host: postgres` as user `postgres` without password. diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md index f5645d586aeda799a7e8fcb0adb13e4dbe1f6af6..08c10d391ea367d0dc553fdfd9ea7a3c375f42d9 100644 --- a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md +++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md @@ -1,5 +1,5 @@ ## Test and Deploy a ruby application -This example will guide you how to run tests in your Ruby application and deploy it automatically as Heroku application. +This example will guide you how to run tests in your Ruby on Rails application and deploy it automatically as Heroku application. You can checkout the example [source](https://gitlab.com/ayufan/ruby-getting-started) and check [CI status](https://gitlab.com/ayufan/ruby-getting-started/builds?scope=all). @@ -32,7 +32,7 @@ production: ``` This project has three jobs: -1. `test` - used to test rails application, +1. `test` - used to test Rails application, 2. `staging` - used to automatically deploy staging environment every push to `master` branch 3. `production` - used to automatically deploy production environmnet for every created tag @@ -62,6 +62,6 @@ gitlab-ci-multi-runner register \ --docker-postgres latest ``` -With the command above, you create a runner that uses [ruby:2.2](https://registry.hub.docker.com/u/library/ruby/) image and uses [postgres](https://registry.hub.docker.com/u/library/postgres/) database. +With the command above, you create a runner that uses [ruby:2.2](https://hub.docker.com/r/_/ruby/) image and uses [postgres](https://hub.docker.com/r/_/postgres/) database. To access PostgreSQL database you need to connect to `host: postgres` as user `postgres` without password. diff --git a/doc/ci/services/mysql.md b/doc/ci/services/mysql.md index c66d77122b2321a99e22d9b4f93171830784cca6..aaf3aa77837998c628da862e38bf14be7e8d6bcc 100644 --- a/doc/ci/services/mysql.md +++ b/doc/ci/services/mysql.md @@ -16,7 +16,7 @@ services: - mysql:latest variables: - # Configure mysql environment variables (https://hub.docker.com/_/mysql/) + # Configure mysql environment variables (https://hub.docker.com/r/_/mysql/) MYSQL_DATABASE: el_duderino MYSQL_ROOT_PASSWORD: mysql_strong_password ``` @@ -114,5 +114,5 @@ available [shared runners](../runners/README.md). Want to hack on it? Simply fork it, commit and push your changes. Within a few moments the changes will be picked by a public runner and the build will begin. -[hub-mysql]: https://hub.docker.com/_/mysql/ +[hub-mysql]: https://hub.docker.com/r/_/mysql/ [mysql-example-repo]: https://gitlab.com/gitlab-examples/mysql diff --git a/doc/ci/services/postgres.md b/doc/ci/services/postgres.md index 17d21dbda1cd5c0d58cef5a19abf33d611fe7855..f787cc0a124f9acbc3438d9b47a357f99eff3ea4 100644 --- a/doc/ci/services/postgres.md +++ b/doc/ci/services/postgres.md @@ -110,5 +110,5 @@ available [shared runners](../runners/README.md). Want to hack on it? Simply fork it, commit and push your changes. Within a few moments the changes will be picked by a public runner and the build will begin. -[hub-pg]: https://hub.docker.com/_/postgres/ +[hub-pg]: https://hub.docker.com/r/_/postgres/ [postgres-example-repo]: https://gitlab.com/gitlab-examples/postgres diff --git a/doc/ci/services/redis.md b/doc/ci/services/redis.md index b281e8f9f604e029c53766a880057f69a6dcbe58..80705024d2f2be0494ded500494dd93b0430d9dc 100644 --- a/doc/ci/services/redis.md +++ b/doc/ci/services/redis.md @@ -65,5 +65,5 @@ that runs on [GitLab.com](https://gitlab.com) using our publicly available Want to hack on it? Simply fork it, commit and push your changes. Within a few moments the changes will be picked by a public runner and the build will begin. -[hub-redis]: https://hub.docker.com/_/redis/ +[hub-redis]: https://hub.docker.com/r/_/redis/ [redis-example-repo]: https://gitlab.com/gitlab-examples/redis diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md index 7f825e6a065780d8d18dfa1890871ea49d3b8098..7c0fb225dac641c55c847d445c247d9fa6df5907 100644 --- a/doc/ci/ssh_keys/README.md +++ b/doc/ci/ssh_keys/README.md @@ -57,7 +57,7 @@ before_script: # WARNING: Use this only with the Docker executor, if you use it with shell # you will overwrite your user's SSH config. - mkdir -p ~/.ssh - - '[[ -f /.dockerinit ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' + - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' ``` As a final step, add the _public_ key from the one you created earlier to the diff --git a/doc/development/README.md b/doc/development/README.md index 3f3ef068f960d543ebb6b65093a32685a67a8a1a..aa7d54c01d0977d366775fbe9d4f52711f47dc10 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -8,6 +8,7 @@ - [How to dump production data to staging](db_dump.md) - [Instrumentation](instrumentation.md) - [Migration Style Guide](migration_style_guide.md) for creating safe migrations +- [Performance guidelines](performance.md) - [Rake tasks](rake_tasks.md) for development - [Shell commands](shell_commands.md) in the GitLab codebase - [Sidekiq debugging](sidekiq_debugging.md) diff --git a/doc/development/performance.md b/doc/development/performance.md new file mode 100644 index 0000000000000000000000000000000000000000..fb37b3a889c73279a615decd94302749d3abc40f --- /dev/null +++ b/doc/development/performance.md @@ -0,0 +1,258 @@ +# Performance Guidelines + +This document describes various guidelines to follow to ensure good and +consistent performance of GitLab. + +## Workflow + +The process of solving performance problems is roughly as follows: + +1. Make sure there's an issue open somewhere (e.g., on the GitLab CE issue + tracker), create one if there isn't. See [#15607][#15607] for an example. +2. Measure the performance of the code in a production environment such as + GitLab.com (see the [Tooling](#tooling) section below). Performance should be + measured over a period of _at least_ 24 hours. +3. Add your findings based on the measurement period (screenshots of graphs, + timings, etc) to the issue mentioned in step 1. +4. Solve the problem. +5. Create a merge request, assign the "performance" label and ping the right + people (e.g. [@yorickpeterse][yorickpeterse] and [@joshfng][joshfng]). +6. Once a change has been deployed make sure to _again_ measure for at least 24 + hours to see if your changes have any impact on the production environment. +7. Repeat until you're done. + +When providing timings make sure to provide: + +* The 95th percentile +* The 99th percentile +* The mean + +When providing screenshots of graphs, make sure that both the X and Y axes and +the legend are clearly visible. If you happen to have access to GitLab.com's own +monitoring tools you should also provide a link to any relevant +graphs/dashboards. + +## Tooling + +GitLab provides two built-in tools to aid the process of improving performance: + +* [Sherlock](doc/development/profiling.md#sherlock) +* [GitLab Performance Monitoring](doc/monitoring/performance/monitoring.md) + +GitLab employees can use GitLab.com's performance monitoring systems located at +<http://performance.gitlab.net>, this requires you to log in using your +`@gitlab.com` Email address. Non-GitLab employees are advised to set up their +own InfluxDB + Grafana stack. + +## Benchmarks + +Benchmarks are almost always useless. Benchmarks usually only test small bits of +code in isolation and often only measure the best case scenario. On top of that, +benchmarks for libraries (e.g., a Gem) tend to be biased in favour of the +library. After all there's little benefit to an author publishing a benchmark +that shows they perform worse than their competitors. + +Benchmarks are only really useful when you need a rough (emphasis on "rough") +understanding of the impact of your changes. For example, if a certain method is +slow a benchmark can be used to see if the changes you're making have any impact +on the method's performance. However, even when a benchmark shows your changes +improve performance there's no guarantee the performance also improves in a +production environment. + +When writing benchmarks you should almost always use +[benchmark-ips](https://github.com/evanphx/benchmark-ips). Ruby's `Benchmark` +module that comes with the standard library is rarely useful as it runs either a +single iteration (when using `Benchmark.bm`) or two iterations (when using +`Benchmark.bmbm`). Running this few iterations means external factors (e.g. a +video streaming in the background) can very easily skew the benchmark +statistics. + +Another problem with the `Benchmark` module is that it displays timings, not +iterations. This means that if a piece of code completes in a very short period +of time it can be very difficult to compare the timings before and after a +certain change. This in turn leads to patterns such as the following: + +```ruby +Benchmark.bmbm(10) do |bench| + bench.report 'do something' do + 100.times do + ... work here ... + end + end +end +``` + +This however leads to the question: how many iterations should we run to get +meaningful statistics? + +The benchmark-ips Gem basically takes care of all this and much more, and as a +result of this should be used instead of the `Benchmark` module. + +In short: + +1. Don't trust benchmarks you find on the internet. +2. Never make claims based on just benchmarks, always measure in production to + confirm your findings. +3. X being N times faster than Y is meaningless if you don't know what impact it + will actually have on your production environment. +4. A production environment is the _only_ benchmark that always tells the truth + (unless your performance monitoring systems are not set up correctly). +5. If you must write a benchmark use the benchmark-ips Gem instead of Ruby's + `Benchmark` module. + +## Importance of Changes + +When working on performance improvements, it's important to always ask yourself +the question "How important is it to improve the performance of this piece of +code?". Not every piece of code is equally important and it would be a waste to +spend a week trying to improve something that only impacts a tiny fraction of +our users. For example, spending a week trying to squeeze 10 milliseconds out of +a method is a waste of time when you could have spent a week squeezing out 10 +seconds elsewhere. + +There is no clear set of steps that you can follow to determine if a certain +piece of code is worth optimizing. The only two things you can do are: + +1. Think about what the code does, how it's used, how many times it's called and + how much time is spent in it relative to the total execution time (e.g., the + total time spent in a web request). +2. Ask others (preferably in the form of an issue). + +Some examples of changes that aren't really important/worth the effort: + +* Replacing double quotes with single quotes. +* Replacing usage of Array with Set when the list of values is very small. +* Replacing library A with library B when both only take up 0.1% of the total + execution time. +* Calling `freeze` on every string (see [String Freezing](#string-freezing)). + +## Slow Operations & Sidekiq + +Slow operations (e.g. merging branches) or operations that are prone to errors +(using external APIs) should be performed in a Sidekiq worker instead of +directly in a web request as much as possible. This has numerous benefits such +as: + +1. An error won't prevent the request from completing. +2. The process being slow won't affect the loading time of a page. +3. In case of a failure it's easy to re-try the process (Sidekiq takes care of + this automatically). +4. By isolating the code from a web request it will hopefully be easier to test + and maintain. + +It's especially important to use Sidekiq as much as possible when dealing with +Git operations as these operations can take quite some time to complete +depending on the performance of the underlying storage system. + +## Git Operations + +Care should be taken to not run unnecessary Git operations. For example, +retrieving the list of branch names using `Repository#branch_names` can be done +without an explicit check if a repository exists or not. In other words, instead +of this: + +```ruby +if repository.exists? + repository.branch_names.each do |name| + ... + end +end +``` + +You can just write: + +```ruby +repository.branch_names.each do |name| + ... +end +``` + +## Caching + +Operations that will often return the same result should be cached using Redis, +in particular Git operations. When caching data in Redis, make sure the cache is +flushed whenever needed. For example, a cache for the list of tags should be +flushed whenever a new tag is pushed or a tag is removed. + +When adding cache expiration code for repositories, this code should be placed +in one of the before/after hooks residing in the Repository class. For example, +if a cache should be flushed after importing a repository this code should be +added to `Repository#after_import`. This ensures the cache logic stays within +the Repository class instead of leaking into other classes. + +When caching data, make sure to also memoize the result in an instance variable. +While retrieving data from Redis is much faster than raw Git operations, it still +has overhead. By caching the result in an instance variable, repeated calls to +the same method won't end up retrieving data from Redis upon every call. When +memoizing cached data in an instance variable, make sure to also reset the +instance variable when flushing the cache. An example: + + +```ruby +def first_branch + @first_branch ||= cache.fetch(:first_branch) { branches.first } +end + +def expire_first_branch_cache + cache.expire(:first_branch) + @first_branch = nil +end +``` + +## Anti-Patterns + +This is a collection of [anti-patterns][anti-pattern] that should be avoided +unless these changes have a measurable, significant and positive impact on +production environments. + +### String Freezing + +In recent Ruby versions calling `freeze` on a String leads to it being allocated +only once and re-used. For example, on Ruby 2.3 this will only allocate the +"foo" String once: + +```ruby +10.times do + 'foo'.freeze +end +``` + +Blindly adding a `.freeze` call to every String is an anti-pattern that should +be avoided unless one can prove (using production data) the call actually has a +positive impact on performance. + +This feature of Ruby wasn't really meant to make things faster directly, instead +it was meant to reduce the number of allocations. Depending on the size of the +String and how frequently it would be allocated (before the `.freeze` call was +added), this _may_ make things faster, but there's no guarantee it will. + +Another common flavour of this is to not only freeze a String, but also assign +it to a constant, for example: + +```ruby +SOME_CONSTANT = 'foo'.freeze + +9000.times do + SOME_CONSTANT +end +``` + +The only reason you should be doing this is to prevent somebody from mutating +the global String. However, since you can just re-assign constants in Ruby +there's nothing stopping somebody from doing this elsewhere in the code: + +```ruby +SOME_CONSTANT = 'bar' +``` + +### Moving Allocations to Constants + +Storing an object as a constant so you only allocate it once _may_ improve +performance, but there's no guarantee this will. Looking up constants has an +impact on runtime performance, and as such, using a constant instead of +referencing an object directly may even slow code down. + +[#15607]: https://gitlab.com/gitlab-org/gitlab-ce/issues/15607 +[yorickpeterse]: https://gitlab.com/u/yorickpeterse +[joshfng]: https://gitlab.com/u/joshfng +[anti-pattern]: https://en.wikipedia.org/wiki/Anti-pattern diff --git a/doc/development/testing.md b/doc/development/testing.md index 672e3fb4649a4f4233bcd9183bbc075f8b1972c1..33eed29ba5c4a08b8b23886947df2c76ea701be9 100644 --- a/doc/development/testing.md +++ b/doc/development/testing.md @@ -64,6 +64,7 @@ the command line via `bundle exec teaspoon`, or via a web browser at methods. - Use `context` to test branching logic. - Don't `describe` symbols (see [Gotchas](gotchas.md#dont-describe-symbols)). +- Don't supply the `:each` argument to hooks since it's the default. - Prefer `not_to` to `to_not`. - Try to match the ordering of tests to the ordering within the class. - Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines diff --git a/doc/install/installation.md b/doc/install/installation.md index e721e70a59615403d0173cd7da3c557e4b3cbdff..e3af302226253b53bb7ce9050adb3fcd5b573970 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -157,22 +157,64 @@ Create a `git` user for GitLab: ## 5. Database -We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](database_mysql.md). *Note*: because we need to make use of extensions you need at least pgsql 9.1. +We recommend using a PostgreSQL database. For MySQL check the +[MySQL setup guide](database_mysql.md). - # Install the database packages - sudo apt-get install -y postgresql postgresql-client libpq-dev +> **Note**: because we need to make use of extensions you need at least pgsql 9.1. - # Create a user for GitLab +1. Install the database packages: + + ```bash + sudo apt-get install -y postgresql postgresql-client libpq-dev postgresql-contrib + ``` + +1. Create a database user for GitLab: + + ```bash sudo -u postgres psql -d template1 -c "CREATE USER git CREATEDB;" + ``` + +1. Create the GitLab production database and grant all privileges on database: - # Create the GitLab production database & grant all privileges on database + ```bash sudo -u postgres psql -d template1 -c "CREATE DATABASE gitlabhq_production OWNER git;" + ``` + +1. Create the `pg_trgm` extension (required for GitLab 8.6+): + + ```bash + sudo -u postgres psql -d template1 -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;" + ``` + +1. Try connecting to the new database with the new user: - # Try connecting to the new database with the new user + ```bash sudo -u git -H psql -d gitlabhq_production + ``` + +1. Check if the `pg_trgm` extension is enabled: + + ```bash + SELECT true AS enabled + FROM pg_available_extensions + WHERE name = 'pg_trgm' + AND installed_version IS NOT NULL; + ``` + + If the extension is enabled this will produce the following output: - # Quit the database session + ``` + enabled + --------- + t + (1 row) + ``` + +1. Quit the database session: + + ```bash gitlabhq_production> \q + ``` ## 6. Redis diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 58f409746cd81c8880962677150e5e7bc9cdf21f..df8e8bdc476b9025f956199a1a55052dc7015aec 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -64,7 +64,10 @@ If you have enough RAM memory and a recent CPU the speed of GitLab is mainly lim ### Memory You need at least 2GB of addressable memory (RAM + swap) to install and use GitLab! -With less memory GitLab will give strange errors during the reconfigure run and 500 errors during usage. +The operating system and any other running applications will also be using memory +so keep in mind that you need at least 2GB available before running GitLab. With +less memory GitLab will give strange errors during the reconfigure run and 500 +errors during usage. - 512MB RAM + 1.5GB of swap is the absolute minimum but we strongly **advise against** this amount of memory. See the unicorn worker section below for more advice. - 1GB RAM + 1GB swap supports up to 100 users but it will be very slow @@ -77,6 +80,10 @@ With less memory GitLab will give strange errors during the reconfigure run and - 128GB RAM supports up to 32,000 users - More users? Run it on [multiple application servers](https://about.gitlab.com/high-availability/) +We recommend having at least 1GB of swap on your server, even if you currently have +enough available RAM. Having swap will help reduce the chance of errors occuring +if your available memory changes. + Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about many you need of those. ## Gitlab Runner diff --git a/doc/integration/github.md b/doc/integration/github.md index e1f9242fd0e416d7e46ad0e2ec4fb11a93792ec4..e7497e475c9a3335f9016f0890152dfd414ce969 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". diff --git a/doc/monitoring/performance/gitlab_configuration.md b/doc/monitoring/performance/gitlab_configuration.md index 90e99302210ee08347e0005d018171aedd831d05..771584268d9106533765710e68154da4419b9566 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 63aa03985ef28b02a748e711aa7929301e222dad..c30cd2950d861bed573cfde89b35366ace2b3b24 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 d31b3788f36ab7795ab2c9f7851b5e5290ba9cc4..41861860b6d2d23ed9a5ec54fdefdcce18c6fcc0 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/update/patch_versions.md b/doc/update/patch_versions.md index 60729316cde31583f227f2e86ad9f8932eff3c56..b4283a526f3d68497c90005ecce125addf6dd04d 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -57,10 +57,10 @@ sudo -u git -H make cd /home/git/gitlab # PostgreSQL -sudo -u git -H bundle install --without development test mysql --deployment +sudo -u git -H bundle install --without development test mysql --with postgres --deployment # MySQL -sudo -u git -H bundle install --without development test postgres --deployment +sudo -u git -H bundle install --without development test postgres --with mysql --deployment # Optional: clean up old gems sudo -u git -H bundle clean diff --git a/docker/README.md b/docker/README.md index 7514d610aecc51143e091d26f2fc392f7764ef0c..ee1f32adc26f9bf26719806598e84931dfa47efc 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,7 +1,7 @@ # GitLab Docker images -* The official GitLab Community Edition Docker image is [available on Docker Hub](https://registry.hub.docker.com/u/gitlab/gitlab-ce/). -* The official GitLab Enterprise Edition Docker image is [available on Docker Hub](https://registry.hub.docker.com/u/gitlab/gitlab-ee/). +* The official GitLab Community Edition Docker image is [available on Docker Hub](https://hub.docker.com/r/gitlab/gitlab-ce/). +* The official GitLab Enterprise Edition Docker image is [available on Docker Hub](https://hub.docker.com/r/gitlab/gitlab-ee/). * The complete usage guide can be found in [Using GitLab Docker images](http://doc.gitlab.com/omnibus/docker/) * The Dockerfile used for building public images is in [Omnibus Repository](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master/docker) -* Check the guide for [creating Omnibus-based Docker Image](http://doc.gitlab.com/omnibus/build/README.html#Build-Docker-image) +* Check the guide for [creating Omnibus-based Docker Image](http://doc.gitlab.com/omnibus/build/README.html#build-docker-image) diff --git a/features/groups.feature b/features/groups.feature index 419a5d3963d6314b0c969653d85d4f3b4d4382ba..49e939807b5258addbd7494e4ff843794516af25 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -7,10 +7,6 @@ Feature: Groups When I visit group "NonExistentGroup" page Then page status code should be 404 - Scenario: I should have back to group button - When I visit group "Owned" page - Then I should see back to dashboard button - @javascript Scenario: I should see group "Owned" dashboard list When I visit group "Owned" page diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb index a167d25983777be8309af1c035edf1646d00d693..f5fddab357df5c64d1d8db5f4a88bd33f59e7368 100644 --- a/features/steps/group/milestones.rb +++ b/features/steps/group/milestones.rb @@ -5,7 +5,9 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps include SharedUser step 'I click on group milestones' do - click_link 'Milestones' + page.within('.layout-nav') do + click_link 'Milestones' + end end step 'I should see group milestones index page has no milestones' do @@ -84,7 +86,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps end step 'I click on the "Labels" tab' do - page.within('.nav-links') do + page.within('.content .nav-links') do page.find(:xpath, "//a[@href='#tab-labels']").click end end diff --git a/features/steps/groups.rb b/features/steps/groups.rb index e5b7db4c5e39a5157f7751dd760c1f49fe0183d4..483370f41c6f775cc43f5081aacd392ff96bbd60 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -4,10 +4,6 @@ class Spinach::Features::Groups < Spinach::FeatureSteps include SharedGroup include SharedUser - step 'I should see back to dashboard button' do - expect(page).to have_content 'Go to dashboard' - end - step 'I should see group "Owned"' do expect(page).to have_content '@owned' end diff --git a/features/steps/project/commits/tags.rb b/features/steps/project/commits/tags.rb index eff4234a44a2487dfd8c718508eca529dfb6bbbc..912ba580efd7a334d314f3b490d2d0b1a76ad419 100644 --- a/features/steps/project/commits/tags.rb +++ b/features/steps/project/commits/tags.rb @@ -57,11 +57,11 @@ class Spinach::Features::ProjectCommitsTags < Spinach::FeatureSteps end step 'I should see new an error that tag ref is invalid' do - expect(page).to have_content 'Invalid reference name' + expect(page).to have_content 'Target foo is invalid' end step 'I should see new an error that tag already exists' do - expect(page).to have_content 'Tag already exists' + expect(page).to have_content 'Tag v1.0.0 already exists' end step "I visit tag 'v1.1.0' page" do diff --git a/features/steps/user.rb b/features/steps/user.rb index 3230234cb6d900417fe2c0467fb0f09a31ad901d..b1d088f07f992e6be6b3f9992a924a0a8f82bc62 100644 --- a/features/steps/user.rb +++ b/features/steps/user.rb @@ -12,7 +12,7 @@ class Spinach::Features::User < Spinach::FeatureSteps user = User.find_by(name: 'John Doe') project = contributed_project - # Issue controbution + # Issue contribution issue_params = { title: 'Bug in old browser' } Issues::CreateService.new(project, user, issue_params).execute @@ -28,7 +28,7 @@ class Spinach::Features::User < Spinach::FeatureSteps end step 'I should see contributed projects' do - page.within '.contributed-projects' do + page.within '#contributed' do expect(page).to have_content(@contributed_project.name) end end diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 4544a41b1e3ca62de7f3fad071e32819d9cbd4ce..93a3a5ce08908ce0355ee4996bb26a54eb1d100e 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -12,14 +12,20 @@ module API # Parameters: # id (required) - The ID of a project # ref_name (optional) - The name of a repository branch or tag, if not given the default branch is used + # since (optional) - Only commits after or in this date will be returned + # until (optional) - Only commits before or in this date will be returned # Example Request: # GET /projects/:id/repository/commits get ":id/repository/commits" do + datetime_attributes! :since, :until + page = (params[:page] || 0).to_i per_page = (params[:per_page] || 20).to_i ref = params[:ref_name] || user_project.try(:default_branch) || 'master' + after = params[:since] + before = params[:until] - commits = user_project.repository.commits(ref, nil, per_page, page * per_page) + commits = user_project.repository.commits(ref, limit: per_page, offset: page * per_page, after: after, before: before) present commits, with: Entities::RepoCommit end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 5bbf721321d71e1f6c67aafe859fd2a119086121..40c967453fb778df98cbf99448817968c55cba91 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -183,6 +183,22 @@ module API Gitlab::Access.options_with_owner.values.include? level.to_i end + # Checks the occurrences of datetime attributes, each attribute if present in the params hash must be in ISO 8601 + # format (YYYY-MM-DDTHH:MM:SSZ) or a Bad Request error is invoked. + # + # Parameters: + # keys (required) - An array consisting of elements that must be parseable as dates from the params hash + def datetime_attributes!(*keys) + keys.each do |key| + begin + params[key] = Time.xmlschema(params[key]) if params[key].present? + rescue ArgumentError + message = "\"" + key.to_s + "\" must be a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ" + render_api_error!(message, 400) + end + end + end + def issuable_order_by if params["order_by"] == 'updated_at' 'updated_at' diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb index 84b4d4cdd6dc2dbfede61517bafa6ea2e18263e3..132043cf3f7d6a917950cd571d5717cbdd00b877 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 22ce3c6a0668ebfa228a0558575c657077704b91..ce1bf0d26d2a56ff53d5522b429c52cac8391242 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/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index 5e2fb863a8fe5d45ce09fa35f8042e6ca73ca3f6..132f9cd1966ba9ed0929237041d1729322b7da6b 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -79,24 +79,6 @@ module Gitlab 'rm-project', "#{name}.git"]) end - # Add repository tag from passed ref - # - # path - project path with namespace - # tag_name - new tag name - # ref - HEAD for new tag - # message - optional message for tag (annotated tag) - # - # Ex. - # add_tag("gitlab/gitlab-ci", "v4.0", "master") - # add_tag("gitlab/gitlab-ci", "v4.0", "master", "message") - # - def add_tag(path, tag_name, ref, message = nil) - cmd = %W(#{gitlab_shell_path}/bin/gitlab-projects create-tag #{path}.git - #{tag_name} #{ref}) - cmd << message unless message.nil? || message.empty? - Gitlab::Utils.system_silent(cmd) - end - # Gc repository # # path - project path with namespace diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb index 9bb507b5edda65e528d0ad5398289b47e6581457..9b83292ef33adceaa56417156d3252581d977254 100644 --- a/lib/gitlab/bitbucket_import/client.rb +++ b/lib/gitlab/bitbucket_import/client.rb @@ -12,7 +12,7 @@ module Gitlab token_secret = import_data_credentials[:bb_session][:bitbucket_access_token_secret] new(token, token_secret) else - raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}" + raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{project.id}" end end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 3ed1eec517c0143b204ae59c793e268ea483a1c0..6cb4123987151b56020fed7f3e7c7f953bc5c843 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -128,7 +128,7 @@ module Gitlab action = if project.protected_branch?(branch_name(ref)) protected_branch_action(oldrev, newrev, branch_name(ref)) - elsif protected_tag?(tag_name(ref)) + elsif (tag_ref = tag_name(ref)) && protected_tag?(tag_ref) # Prevent any changes to existing git tag unless user has permissions :admin_project else @@ -176,7 +176,7 @@ module Gitlab end def protected_tag?(tag_name) - project.repository.tag_names.include?(tag_name) + project.repository.tag_exists?(tag_name) end def user_allowed? diff --git a/lib/gitlab/github_import/comment_formatter.rb b/lib/gitlab/github_import/comment_formatter.rb index 7d58e53991a24773b510d3a6c0367ff8cb9db98c..7d679eaec6aebb436d28cde191f5607ec775136a 100644 --- a/lib/gitlab/github_import/comment_formatter.rb +++ b/lib/gitlab/github_import/comment_formatter.rb @@ -28,13 +28,26 @@ module Gitlab end def line_code - if on_diff? - Gitlab::Diff::LineCode.generate(raw_data.path, raw_data.position, 0) - end + return unless on_diff? + + parsed_lines = Gitlab::Diff::Parser.new.parse(diff_hunk.lines) + generate_line_code(parsed_lines.to_a.last) + end + + def generate_line_code(line) + Gitlab::Diff::LineCode.generate(file_path, line.new_pos, line.old_pos) end def on_diff? - raw_data.path && raw_data.position + diff_hunk.present? + end + + def diff_hunk + raw_data.diff_hunk + end + + def file_path + raw_data.path end def note diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb index 67622f321a69e014f53924d6ea3b044ff952e012..c8f12577112eaed1562bb49cc7b31b0a0b477090 100644 --- a/lib/gitlab/push_data_builder.rb +++ b/lib/gitlab/push_data_builder.rb @@ -66,7 +66,7 @@ module Gitlab # This method provide a sample data generated with # existing project and commits to test webhooks def build_sample(project, user) - commits = project.repository.commits(project.default_branch, nil, 3) + commits = project.repository.commits(project.default_branch, limit: 3) ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{project.default_branch}" build(project, user, commits.last.id, commits.first.id, ref, commits) end diff --git a/lib/gitlab/sanitizers/svg.rb b/lib/gitlab/sanitizers/svg.rb new file mode 100644 index 0000000000000000000000000000000000000000..b98589dff89b3aa45cb128c100d60d67f5e13527 --- /dev/null +++ b/lib/gitlab/sanitizers/svg.rb @@ -0,0 +1,37 @@ +require_relative "svg/whitelist" + +module Gitlab + module Sanitizers + module SVG + def self.clean(data) + Loofah.xml_document(data).scrub!(Scrubber.new).to_s + end + + class Scrubber < Loofah::Scrubber + # http://www.whatwg.org/specs/web-apps/current-work/multipage/elements.html#embedding-custom-non-visible-data-with-the-data-*-attributes + DATA_ATTR_PATTERN = /\Adata-(?!xml)[a-z_][\w.\u00E0-\u00F6\u00F8-\u017F\u01DD-\u02AF-]*\z/u + + def scrub(node) + unless ALLOWED_ELEMENTS.include?(node.name) + node.unlink + else + node.attributes.each do |attr_name, attr| + valid_attributes = ALLOWED_ATTRIBUTES[node.name] + + unless valid_attributes && valid_attributes.include?(attr_name) + if ALLOWED_DATA_ATTRIBUTES_IN_ELEMENTS.include?(node.name) && + attr_name.start_with?('data-') + # Arbitrary data attributes are allowed. Verify that the attribute + # is a valid data attribute. + attr.unlink unless attr_name =~ DATA_ATTR_PATTERN + else + attr.unlink + end + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/sanitizers/svg/whitelist.rb b/lib/gitlab/sanitizers/svg/whitelist.rb new file mode 100644 index 0000000000000000000000000000000000000000..917e795b29e2ba3c6ded41850792f3f79f86d595 --- /dev/null +++ b/lib/gitlab/sanitizers/svg/whitelist.rb @@ -0,0 +1,107 @@ +# Generated from: +# SVG element list: https://www.w3.org/TR/SVG/eltindex.html +# SVG Attribute list: https://www.w3.org/TR/SVG/attindex.html +module Gitlab + module Sanitizers + module SVG + ALLOWED_ELEMENTS = %w[ + a altGlyph altGlyphDef altGlyphItem animate + animateColor animateMotion animateTransform circle clipPath color-profile + cursor defs desc ellipse feBlend feColorMatrix feComponentTransfer + feComposite feConvolveMatrix feDiffuseLighting feDisplacementMap + feDistantLight feFlood feFuncA feFuncB feFuncG feFuncR feGaussianBlur + feImage feMerge feMergeNode feMorphology feOffset fePointLight + feSpecularLighting feSpotLight feTile feTurbulence filter font font-face + font-face-format font-face-name font-face-src font-face-uri foreignObject + g glyph glyphRef hkern image line linearGradient marker mask metadata + missing-glyph mpath path pattern polygon polyline radialGradient rect + script set stop style svg switch symbol text textPath title tref tspan use + view vkern].freeze + + ALLOWED_DATA_ATTRIBUTES_IN_ELEMENTS = %w[svg].freeze + + ALLOWED_ATTRIBUTES = { + 'a' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage target text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'altGlyph' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format glyph-orientation-horizontal glyph-orientation-vertical glyphRef id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'altGlyphDef' => %w[id xml:base xml:lang xml:space], + 'altGlyphItem' => %w[id xml:base xml:lang xml:space], + 'animate' => %w[accumulate additive alignment-baseline attributeName attributeType baseline-shift begin by calcMode clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dur enable-background end externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight from glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning keySplines keyTimes letter-spacing lighting-color marker-end marker-mid marker-start mask max min onbegin onend onload onrepeat opacity overflow pointer-events repeatCount repeatDur requiredExtensions requiredFeatures restart shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width systemLanguage text-anchor text-decoration text-rendering to unicode-bidi values visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'animateColor' => %w[accumulate additive alignment-baseline attributeName attributeType baseline-shift begin by calcMode clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dur enable-background end externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight from glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning keySplines keyTimes letter-spacing lighting-color marker-end marker-mid marker-start mask max min onbegin onend onload onrepeat opacity overflow pointer-events repeatCount repeatDur requiredExtensions requiredFeatures restart shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width systemLanguage text-anchor text-decoration text-rendering to unicode-bidi values visibility word-spacing writing-mode xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'animateMotion' => %w[accumulate additive begin by calcMode dur end externalResourcesRequired fill from id keyPoints keySplines keyTimes max min onbegin onend onload onrepeat origin path repeatCount repeatDur requiredExtensions requiredFeatures restart rotate systemLanguage to values xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'animateTransform' => %w[accumulate additive attributeName attributeType begin by calcMode dur end externalResourcesRequired fill from id keySplines keyTimes max min onbegin onend onload onrepeat repeatCount repeatDur requiredExtensions requiredFeatures restart systemLanguage to type values xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'circle' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events r requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'clipPath' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule clipPathUnits color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'color-profile' => %w[id local name rendering-intent xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'cursor' => %w[externalResourcesRequired id requiredExtensions requiredFeatures systemLanguage x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'defs' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'desc' => %w[class id style xml:base xml:lang xml:space], + 'ellipse' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rx ry shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'feBlend' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask mode opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feColorMatrix' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering type unicode-bidi values visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feComponentTransfer' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feComposite' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 k1 k2 k3 k4 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity operator overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feConvolveMatrix' => %w[alignment-baseline baseline-shift bias class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display divisor dominant-baseline edgeMode enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelMatrix kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity order overflow pointer-events preserveAlpha result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style targetX targetY text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feDiffuseLighting' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor diffuseConstant direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feDisplacementMap' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in in2 kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result scale shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xChannelSelector xml:base xml:lang xml:space y yChannelSelector], + 'feDistantLight' => %w[azimuth elevation id xml:base xml:lang xml:space], + 'feFlood' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feFuncA' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], + 'feFuncB' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], + 'feFuncG' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], + 'feFuncR' => %w[amplitude exponent id intercept offset slope tableValues type xml:base xml:lang xml:space], + 'feGaussianBlur' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stdDeviation stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feImage' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events preserveAspectRatio result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'feMerge' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feMergeNode' => %w[id xml:base xml:lang xml:space], + 'feMorphology' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity operator overflow pointer-events radius result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feOffset' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'fePointLight' => %w[id x xml:base xml:lang xml:space y z], + 'feSpecularLighting' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kernelUnitLength kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering specularConstant specularExponent stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feSpotLight' => %w[id limitingConeAngle pointsAtX pointsAtY pointsAtZ specularExponent x xml:base xml:lang xml:space y z], + 'feTile' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering in kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events result shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'feTurbulence' => %w[alignment-baseline baseFrequency baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask numOctaves opacity overflow pointer-events result seed shape-rendering stitchTiles stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering type unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'filter' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter filterRes filterUnits flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events primitiveUnits shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'font' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x horiz-origin-y id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'font-face' => %w[accent-height alphabetic ascent bbox cap-height descent font-family font-size font-stretch font-style font-variant font-weight hanging id ideographic mathematical overline-position overline-thickness panose-1 slope stemh stemv strikethrough-position strikethrough-thickness underline-position underline-thickness unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical widths x-height xml:base xml:lang xml:space], + 'font-face-format' => %w[id string xml:base xml:lang xml:space], + 'font-face-name' => %w[id name xml:base xml:lang xml:space], + 'font-face-src' => %w[id xml:base xml:lang xml:space], + 'font-face-uri' => %w[id xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'foreignObject' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'g' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'glyph' => %w[alignment-baseline arabic-form baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x id image-rendering kerning lang letter-spacing lighting-color marker-end marker-mid marker-start mask opacity orientation overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'glyphRef' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format glyph-orientation-horizontal glyph-orientation-vertical glyphRef id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'hkern' => %w[g1 g2 id k u1 u2 xml:base xml:lang xml:space], + 'image' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'line' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode x1 x2 xml:base xml:lang xml:space y1 y2], + 'linearGradient' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical gradientTransform gradientUnits id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering spreadMethod stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode x1 x2 xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space y1 y2], + 'marker' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start markerHeight markerUnits markerWidth mask opacity orient overflow pointer-events preserveAspectRatio refX refY shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi viewBox visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'mask' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask maskContentUnits maskUnits opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'metadata' => %w[id xml:base xml:lang xml:space], + 'missing-glyph' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi vert-adv-y vert-origin-x vert-origin-y visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'mpath' => %w[externalResourcesRequired id xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'path' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor d direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pathLength pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'pattern' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow patternContentUnits patternTransform patternUnits pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi viewBox visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'polygon' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events points requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'polyline' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events points requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'radialGradient' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor cx cy direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight fx fy glyph-orientation-horizontal glyph-orientation-vertical gradientTransform gradientUnits id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask opacity overflow pointer-events r shape-rendering spreadMethod stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space], + 'rect' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rx ry shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xml:base xml:lang xml:space y], + 'script' => %w[externalResourcesRequired id type xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'set' => %w[attributeName attributeType begin dur end externalResourcesRequired fill id max min onbegin onend onload onrepeat repeatCount repeatDur requiredExtensions requiredFeatures restart systemLanguage to xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space], + 'stop' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask offset opacity overflow pointer-events shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'style' => %w[id media title type xml:base xml:lang xml:space], + 'svg' => %w[alignment-baseline baseProfile baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering contentScriptType contentStyleType cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onabort onactivate onclick onerror onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup onresize onscroll onunload onzoom opacity overflow pointer-events preserveAspectRatio requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering unicode-bidi version viewBox visibility width word-spacing writing-mode x xml:base xml:lang xml:space xmlns y zoomAndPan], + 'switch' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'symbol' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events preserveAspectRatio shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style text-anchor text-decoration text-rendering unicode-bidi viewBox visibility word-spacing writing-mode xml:base xml:lang xml:space], + 'text' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength transform unicode-bidi visibility word-spacing writing-mode x xml:base xml:lang xml:space y], + 'textPath' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask method onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering spacing startOffset stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space], + 'title' => %w[class id style xml:base xml:lang xml:space], + 'tref' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode x xlink:arcrole xlink:href xlink:role xlink:title xlink:type xml:base xml:lang xml:space y], + 'tspan' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline dx dy enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical id image-rendering kerning lengthAdjust letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures rotate shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering textLength unicode-bidi visibility word-spacing writing-mode x xml:base xml:lang xml:space y], + 'use' => %w[alignment-baseline baseline-shift class clip clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering cursor direction display dominant-baseline enable-background externalResourcesRequired fill fill-opacity fill-rule filter flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-orientation-horizontal glyph-orientation-vertical height id image-rendering kerning letter-spacing lighting-color marker-end marker-mid marker-start mask onactivate onclick onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup opacity overflow pointer-events requiredExtensions requiredFeatures shape-rendering stop-color stop-opacity stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style systemLanguage text-anchor text-decoration text-rendering transform unicode-bidi visibility width word-spacing writing-mode x xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y], + 'view' => %w[externalResourcesRequired id preserveAspectRatio viewBox viewTarget xml:base xml:lang xml:space zoomAndPan], + 'vkern' => %w[g1 g2 id k u1 u2 xml:base xml:lang xml:space] + }.freeze + end + end +end diff --git a/lib/tasks/auto_annotate_models.rake b/lib/tasks/auto_annotate_models.rake new file mode 100644 index 0000000000000000000000000000000000000000..16bad4bd2bdcf895aed941a574521200b659b38a --- /dev/null +++ b/lib/tasks/auto_annotate_models.rake @@ -0,0 +1,44 @@ +if Rails.env.development? + task :set_annotation_options do + # You can override any of these by setting an environment variable of the + # same name. + Annotate.set_defaults( + 'routes' => 'false', + 'position_in_routes' => 'before', + 'position_in_class' => 'before', + 'position_in_test' => 'before', + 'position_in_fixture' => 'before', + 'position_in_factory' => 'before', + 'position_in_serializer' => 'before', + 'show_foreign_keys' => 'true', + 'show_indexes' => 'false', + 'simple_indexes' => 'false', + 'model_dir' => 'app/models', + 'root_dir' => '', + 'include_version' => 'false', + 'require' => '', + 'exclude_tests' => 'true', + 'exclude_fixtures' => 'true', + 'exclude_factories' => 'true', + 'exclude_serializers' => 'true', + 'exclude_scaffolds' => 'true', + 'exclude_controllers' => 'true', + 'exclude_helpers' => 'true', + 'ignore_model_sub_dir' => 'false', + 'ignore_columns' => nil, + 'ignore_unknown_models' => 'false', + 'hide_limit_column_types' => 'integer,boolean', + 'skip_on_db_migrate' => 'false', + 'format_bare' => 'true', + 'format_rdoc' => 'false', + 'format_markdown' => 'false', + 'sort' => 'false', + 'force' => 'false', + 'trace' => 'false', + 'wrapper_open' => nil, + 'wrapper_close' => nil, + ) + end + + Annotate.load_tasks +end diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake index 4921c6e0bcf4ab43151b6543de680c8dc5f99b80..1c706dc11b33ece40c46f0ca618e9b042da2a93d 100644 --- a/lib/tasks/gitlab/db.rake +++ b/lib/tasks/gitlab/db.rake @@ -29,7 +29,10 @@ namespace :gitlab do tables.delete 'schema_migrations' # Truncate schema_migrations to ensure migrations re-run connection.execute('TRUNCATE schema_migrations') - tables.each { |t| connection.execute("DROP TABLE #{t}") } + # Drop tables with cascade to avoid dependent table errors + # PG: http://www.postgresql.org/docs/current/static/ddl-depend.html + # MySQL: http://dev.mysql.com/doc/refman/5.7/en/drop-table.html + tables.each { |t| connection.execute("DROP TABLE #{t} CASCADE") } end end end diff --git a/spec/controllers/admin/impersonation_controller_spec.rb b/spec/controllers/admin/impersonation_controller_spec.rb deleted file mode 100644 index d7a7ba1c5b6601194dac6a17ca0e7e58e3c40e00..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..eb82476b179b56db1f455015863d52be2a94a76d --- /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 9ef8ba1b09763fac21972a2480d790600c86b46e..ce2a62ae1fd3319d7b03ac62d7c579a1eca07264 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/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index d6e4cd71ce6b11233b18f130142cf62b41b96283..2b2ad3b9412ab3a78b2fc3fc03b0e9c1c98bb59c 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 94dd935a0393cd7dbde3ef9c70ccc768530b6806..3195fb3ddcc90332facafc6ccb5bfd7b80a6d7f1 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 0000000000000000000000000000000000000000..24e83d44010cb0bc7fdd511e0c30427447c14b91 --- /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_spec.rb b/spec/features/issues_spec.rb index b57131f68d57ca08a79f85fee0761e0f7f8c78a7..d5755c293c53a75c6027970c2dc04838117eb032 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -264,12 +264,14 @@ describe 'Issues', feature: true do visit namespace_project_issues_path(project.namespace, project, sort: sort_value_milestone_soon) expect(first_issue).to include('foo') + expect(last_issue).to include('baz') end it 'sorts by least recently due milestone' do visit namespace_project_issues_path(project.namespace, project, sort: sort_value_milestone_later) expect(first_issue).to include('bar') + expect(last_issue).to include('baz') end end diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index 4433ef2d6f116f0fa7b932bf97135eaf649008e0..8c38dd5b122eec74a169314be9577e278e3e0d15 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -37,7 +37,7 @@ feature 'Login', feature: true do end def enter_code(code) - fill_in 'Two-factor authentication code', with: code + fill_in 'Two-factor Authentication code', with: code click_button 'Verify code' end diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index 00b60bd0e7564551a175c7bc64f908de34ecef67..e296078bad80fb8ed4e85cffe3fe727264a39c2e 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/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7e6eef6587379b2c96c0dd202329caee77d4350a --- /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 0000000000000000000000000000000000000000..ef82d2375dd9bd22a74eee27f80671b0fc6d40d9 --- /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/signup_spec.rb b/spec/features/signup_spec.rb index 51b754ff85c54e3d69ac6817db6f27b80ec9104a..58aabd913ebcb150780c11a7c8f9a906fd22b524 100644 --- a/spec/features/signup_spec.rb +++ b/spec/features/signup_spec.rb @@ -7,10 +7,10 @@ feature 'Signup', feature: true do visit root_path - fill_in 'user_name', with: user.name - fill_in 'user_username', with: user.username - fill_in 'user_email', with: user.email - fill_in 'user_password_sign_up', with: user.password + fill_in 'new_user_name', with: user.name + fill_in 'new_user_username', with: user.username + fill_in 'new_user_email', with: user.email + fill_in 'new_user_password', with: user.password click_button "Sign up" expect(current_path).to eq users_almost_there_path @@ -25,10 +25,10 @@ feature 'Signup', feature: true do visit root_path - fill_in 'user_name', with: user.name - fill_in 'user_username', with: user.username - fill_in 'user_email', with: existing_user.email - fill_in 'user_password_sign_up', with: user.password + fill_in 'new_user_name', with: user.name + fill_in 'new_user_username', with: user.username + fill_in 'new_user_email', with: existing_user.email + fill_in 'new_user_password', with: user.password click_button "Sign up" expect(current_path).to eq user_registration_path @@ -42,10 +42,10 @@ feature 'Signup', feature: true do visit root_path - fill_in 'user_name', with: user.name - fill_in 'user_username', with: user.username - fill_in 'user_email', with: existing_user.email - fill_in 'user_password_sign_up', with: user.password + fill_in 'new_user_name', with: user.name + fill_in 'new_user_username', with: user.username + fill_in 'new_user_email', with: existing_user.email + fill_in 'new_user_password', with: user.password click_button "Sign up" expect(current_path).to eq user_registration_path diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb index c124816203189592e6fb861c3335d642d32f776e..cf116040394fd289ca74c47a5569eec74c09fe62 100644 --- a/spec/features/users_spec.rb +++ b/spec/features/users_spec.rb @@ -5,10 +5,10 @@ feature 'Users', feature: true do scenario 'GET /users/sign_in creates a new user account' do visit new_user_session_path - fill_in 'user_name', with: 'Name Surname' - fill_in 'user_username', with: 'Great' - fill_in 'user_email', with: 'name@mail.com' - fill_in 'user_password_sign_up', with: 'password1234' + fill_in 'new_user_name', with: 'Name Surname' + fill_in 'new_user_username', with: 'Great' + fill_in 'new_user_email', with: 'name@mail.com' + fill_in 'new_user_password', with: 'password1234' expect { click_button 'Sign up' }.to change { User.count }.by(1) end @@ -31,10 +31,10 @@ feature 'Users', feature: true do scenario 'Should show one error if email is already taken' do visit new_user_session_path - fill_in 'user_name', with: 'Another user name' - fill_in 'user_username', with: 'anotheruser' - fill_in 'user_email', with: user.email - fill_in 'user_password_sign_up', with: '12341234' + fill_in 'new_user_name', with: 'Another user name' + fill_in 'new_user_username', with: 'anotheruser' + fill_in 'new_user_email', with: user.email + fill_in 'new_user_password', with: '12341234' expect { click_button 'Sign up' }.to change { User.count }.by(0) expect(page).to have_text('Email has already been taken') expect(number_of_errors_on_page(page)).to be(1), 'errors on page:\n #{errors_on_page page}' diff --git a/spec/fixtures/sanitized.svg b/spec/fixtures/sanitized.svg new file mode 100644 index 0000000000000000000000000000000000000000..8f84b8f5e206aaa0f63b930d2f94c0acfeb3d530 --- /dev/null +++ b/spec/fixtures/sanitized.svg @@ -0,0 +1,50 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 622 682"> + + <defs> + <style>.cls-1{fill:#30353e;}.cls-2{fill:#8c929d;}.cls-3{fill:#fc6d26;}.cls-4{fill:#e24329;}.cls-5{fill:#fca326;}</style> + </defs> + <title>stacked_wm</title> + <path id="bg" class="cls-1" d="M622,681H0V-1H622V681h0Z"/> + <g id="g12"> + <path id="path14" class="cls-2" d="M316.89,497.72h-19l0.06,141.74H375V621.93h-58l-0.06-124.22h0Z"/> + </g> + <g id="g24"> + <path id="path26" class="cls-2" d="M448.32,614.57a32.46,32.46,0,0,1-23.59,10c-14.5,0-20.35-7.14-20.35-16.45,0-14.07,9.74-20.77,30.52-20.77a86.46,86.46,0,0,1,13.42,1.08v26.19h0Zm-19.7-85.91a63.45,63.45,0,0,0-40.5,14.53l6.73,11.66c7.79-4.54,17.32-9.09,31-9.09,15.58,0,22.51,8,22.51,21.42v6.93a81.48,81.48,0,0,0-13.2-1.08c-33.33,0-50.22,11.69-50.22,36.14,0,21.86,13.42,32.89,33.76,32.89,13.71,0,26.84-6.28,31.38-16.45l3.46,13.85h13.42V567c0-22.94-10-38.3-38.31-38.3h0Z"/> + </g> + <g id="g28"> + <path id="path30" class="cls-2" d="M528.4,625.18c-7.14,0-13.42-.87-18.18-3V556.58c6.49-5.41,14.5-9.31,24.68-9.31,18.4,0,25.54,13,25.54,34,0,29.86-11.47,43.93-32,43.93m8-96.52a34.88,34.88,0,0,0-26.19,11.58V522l-0.06-24.24H491.54L491.6,636c9.31,3.9,22.08,6.06,35.93,6.06,35.5,0,52.6-22.72,52.6-61.89,0-30.95-15.8-51.51-43.73-51.51"/> + </g> + <g id="g32"> + <path id="path34" class="cls-2" d="M109.84,513.08c16.88,0,27.7,5.63,34.85,11.25l8.19-14.18c-11.16-9.78-26.16-15-42.17-15-40.47,0-68.83,24.67-68.83,74.44,0,52.15,30.59,72.5,65.58,72.5a111,111,0,0,0,42.21-8.22l-0.4-55.72V560.58H97.32v17.53h33.12l0.4,42.31c-4.33,2.16-11.9,3.9-22.08,3.9-28.14,0-47-17.7-47-55,0-37.87,19.48-56.26,48.05-56.26"/> + </g> + <g id="g36"> + <path id="path38" class="cls-2" d="M243.79,497.72H225.17l0.06,23.8v82.23c0,22.94,10,38.3,38.31,38.3A64.16,64.16,0,0,0,275,641V624.31a57,57,0,0,1-8.66.65c-15.58,0-22.51-8-22.51-21.42v-56.7H275V531.26H243.85l-0.06-33.54h0Z"/> + </g> + <path id="path40" class="cls-2" d="M177.94,639.46h18.61V531.26H177.94v108.2h0Z"/> + <path id="path42" class="cls-2" d="M177.94,516.33h18.61V497.72H177.94v18.61h0Z"/> + <g id="g44"> + <path id="path46" class="cls-3" d="M525.05,266.23l-24-74L453.36,45.6a8.19,8.19,0,0,0-15.58,0L390.12,192.24H231.88L184.22,45.6a8.19,8.19,0,0,0-15.58,0L121,192.24l-24,74a16.38,16.38,0,0,0,6,18.31L311,435.71,519.1,284.54a16.38,16.38,0,0,0,6-18.31"/> + </g> + <g id="g48"> + <path id="path50" class="cls-4" d="M311,435.71h0l79.12-243.47H231.88L311,435.71h0Z"/> + </g> + <g id="g56"> + <path id="path58" class="cls-3" d="M311,435.71L231.88,192.24H121L311,435.71h0Z"/> + </g> + <g id="g64"> + <path id="path66" class="cls-5" d="M121,192.24h0l-24,74a16.37,16.37,0,0,0,6,18.31L311,435.7,121,192.24h0Z"/> + </g> + <g id="g72"> + <path id="path74" class="cls-4" d="M121,192.24H231.88L184.22,45.6a8.19,8.19,0,0,0-15.58,0L121,192.24h0Z"/> + </g> + <g id="g76"> + <path id="path78" class="cls-3" d="M311,435.71l79.12-243.47H501L311,435.71h0Z"/> + </g> + <g id="g80"> + <path id="path82" class="cls-5" d="M501,192.24h0l24,74a16.37,16.37,0,0,1-6,18.31L311,435.7,501,192.24h0Z"/> + </g> + <g id="g84"> + <path id="path86" class="cls-4" d="M501,192.24H390.12L437.78,45.6a8.19,8.19,0,0,1,15.58,0L501,192.24h0Z"/> + </g> +</svg> diff --git a/spec/fixtures/unsanitized.svg b/spec/fixtures/unsanitized.svg new file mode 100644 index 0000000000000000000000000000000000000000..3957557334bbbedebc51a274e342201526ba2594 --- /dev/null +++ b/spec/fixtures/unsanitized.svg @@ -0,0 +1,50 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 622 682" filterMe="test"> + <iframe src="http://www.google.com"></iframe> + <defs> + <style>.cls-1{fill:#30353e;}.cls-2{fill:#8c929d;}.cls-3{fill:#fc6d26;}.cls-4{fill:#e24329;}.cls-5{fill:#fca326;}</style> + </defs> + <title>stacked_wm</title> + <path id="bg" class="cls-1" d="M622,681H0V-1H622V681h0Z"/> + <g id="g12"> + <path id="path14" class="cls-2" d="M316.89,497.72h-19l0.06,141.74H375V621.93h-58l-0.06-124.22h0Z"/> + </g> + <g id="g24"> + <path id="path26" class="cls-2" d="M448.32,614.57a32.46,32.46,0,0,1-23.59,10c-14.5,0-20.35-7.14-20.35-16.45,0-14.07,9.74-20.77,30.52-20.77a86.46,86.46,0,0,1,13.42,1.08v26.19h0Zm-19.7-85.91a63.45,63.45,0,0,0-40.5,14.53l6.73,11.66c7.79-4.54,17.32-9.09,31-9.09,15.58,0,22.51,8,22.51,21.42v6.93a81.48,81.48,0,0,0-13.2-1.08c-33.33,0-50.22,11.69-50.22,36.14,0,21.86,13.42,32.89,33.76,32.89,13.71,0,26.84-6.28,31.38-16.45l3.46,13.85h13.42V567c0-22.94-10-38.3-38.31-38.3h0Z"/> + </g> + <g id="g28"> + <path id="path30" class="cls-2" d="M528.4,625.18c-7.14,0-13.42-.87-18.18-3V556.58c6.49-5.41,14.5-9.31,24.68-9.31,18.4,0,25.54,13,25.54,34,0,29.86-11.47,43.93-32,43.93m8-96.52a34.88,34.88,0,0,0-26.19,11.58V522l-0.06-24.24H491.54L491.6,636c9.31,3.9,22.08,6.06,35.93,6.06,35.5,0,52.6-22.72,52.6-61.89,0-30.95-15.8-51.51-43.73-51.51"/> + </g> + <g id="g32"> + <path id="path34" class="cls-2" d="M109.84,513.08c16.88,0,27.7,5.63,34.85,11.25l8.19-14.18c-11.16-9.78-26.16-15-42.17-15-40.47,0-68.83,24.67-68.83,74.44,0,52.15,30.59,72.5,65.58,72.5a111,111,0,0,0,42.21-8.22l-0.4-55.72V560.58H97.32v17.53h33.12l0.4,42.31c-4.33,2.16-11.9,3.9-22.08,3.9-28.14,0-47-17.7-47-55,0-37.87,19.48-56.26,48.05-56.26"/> + </g> + <g id="g36"> + <path id="path38" class="cls-2" d="M243.79,497.72H225.17l0.06,23.8v82.23c0,22.94,10,38.3,38.31,38.3A64.16,64.16,0,0,0,275,641V624.31a57,57,0,0,1-8.66.65c-15.58,0-22.51-8-22.51-21.42v-56.7H275V531.26H243.85l-0.06-33.54h0Z"/> + </g> + <path id="path40" class="cls-2" d="M177.94,639.46h18.61V531.26H177.94v108.2h0Z"/> + <path id="path42" class="cls-2" d="M177.94,516.33h18.61V497.72H177.94v18.61h0Z"/> + <g id="g44"> + <path id="path46" class="cls-3" d="M525.05,266.23l-24-74L453.36,45.6a8.19,8.19,0,0,0-15.58,0L390.12,192.24H231.88L184.22,45.6a8.19,8.19,0,0,0-15.58,0L121,192.24l-24,74a16.38,16.38,0,0,0,6,18.31L311,435.71,519.1,284.54a16.38,16.38,0,0,0,6-18.31"/> + </g> + <g id="g48"> + <path id="path50" class="cls-4" d="M311,435.71h0l79.12-243.47H231.88L311,435.71h0Z"/> + </g> + <g id="g56"> + <path id="path58" class="cls-3" d="M311,435.71L231.88,192.24H121L311,435.71h0Z"/> + </g> + <g id="g64"> + <path id="path66" class="cls-5" d="M121,192.24h0l-24,74a16.37,16.37,0,0,0,6,18.31L311,435.7,121,192.24h0Z"/> + </g> + <g id="g72"> + <path id="path74" class="cls-4" d="M121,192.24H231.88L184.22,45.6a8.19,8.19,0,0,0-15.58,0L121,192.24h0Z"/> + </g> + <g id="g76"> + <path id="path78" class="cls-3" d="M311,435.71l79.12-243.47H501L311,435.71h0Z"/> + </g> + <g id="g80"> + <path id="path82" class="cls-5" d="M501,192.24h0l24,74a16.37,16.37,0,0,1-6,18.31L311,435.7,501,192.24h0Z"/> + </g> + <g id="g84"> + <path id="path86" class="cls-4" d="M501,192.24H390.12L437.78,45.6a8.19,8.19,0,0,1,15.58,0L501,192.24h0Z"/> + </g> +</svg> diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index 87849230dbe97b883591d92e7330ed486a82d239..6d1c02db297926e9f6d15158e08787a107f0ddef 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -67,4 +67,16 @@ describe BlobHelper do expect(result).to eq(expected) end end + + describe "#sanitize_svg" do + let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') } + let(:data) { open(input_svg_path).read } + let(:expected_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'sanitized.svg') } + let(:expected) { open(expected_svg_path).read } + + it 'should retain essential elements' do + blob = OpenStruct.new(data: data) + expect(sanitize_svg(blob).data).to eq(expected) + end + end end diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index 543593cf389cfb2fe71b63a34f278f329cbb1091..bffe2c18b6f90d86d801d3455ae13a8a466ab195 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/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 62389188d2c8add2479e86b6bf0f1c13017776d5..29bcb8c58924de7bc75ef972585d9a7e2ff1ed90 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -131,4 +131,13 @@ describe ProjectsHelper do end end end + + describe '#sanitized_import_error' do + it 'removes the repo path' do + repo = File.join(Gitlab.config.gitlab_shell.repos_path, '/namespace/test.git') + import_error = "Could not clone #{repo}\n" + + expect(sanitize_repo_path(import_error)).to eq('Could not clone [REPOS PATH]/namespace/test.git') + end + end end diff --git a/spec/javascripts/merge_request_widget_spec.js.coffee b/spec/javascripts/merge_request_widget_spec.js.coffee new file mode 100644 index 0000000000000000000000000000000000000000..c0bd8a29e43e8e93a4e54314e11a93997a60f96f --- /dev/null +++ b/spec/javascripts/merge_request_widget_spec.js.coffee @@ -0,0 +1,49 @@ +#= require merge_request_widget + +describe 'MergeRequestWidget', -> + + beforeEach -> + window.notifyPermissions = () -> + window.notify = () -> + @opts = { + ci_status_url:"http://sampledomain.local/ci/getstatus", + ci_status:"", + ci_message: { + normal: "Build {{status}} for \"{{title}}\"", + preparing: "{{status}} build for \"{{title}}\"" + }, + ci_title: { + preparing: "{{status}} build", + normal: "Build {{status}}" + }, + gitlab_icon:"gitlab_logo.png", + builds_path:"http://sampledomain.local/sampleBuildsPath" + } + @class = new MergeRequestWidget(@opts) + @ciStatusData = {"title":"Sample MR title","sha":"12a34bc5","status":"success","coverage":98} + + describe 'getCIStatus', -> + beforeEach -> + spyOn(jQuery, 'getJSON').and.callFake (req, cb) => + cb(@ciStatusData) + + it 'should call showCIStatus even if a notification should not be displayed', -> + spy = spyOn(@class, 'showCIStatus').and.stub() + @class.getCIStatus(false) + expect(spy).toHaveBeenCalledWith(@ciStatusData.status) + + it 'should call showCIStatus when a notification should be displayed', -> + spy = spyOn(@class, 'showCIStatus').and.stub() + @class.getCIStatus(true) + expect(spy).toHaveBeenCalledWith(@ciStatusData.status) + + it 'should call showCICoverage when the coverage rate is set', -> + spy = spyOn(@class, 'showCICoverage').and.stub() + @class.getCIStatus(false) + expect(spy).toHaveBeenCalledWith(@ciStatusData.coverage) + + it 'should not call showCICoverage when the coverage rate is not set', -> + @ciStatusData.coverage = null + spy = spyOn(@class, 'showCICoverage').and.stub() + @class.getCIStatus(false) + expect(spy).not.toHaveBeenCalled() diff --git a/spec/lib/gitlab/bitbucket_import/client_spec.rb b/spec/lib/gitlab/bitbucket_import/client_spec.rb index aa0699f2ebf99634602421c3118c669d9a8d2ea1..af839f4242155a036fbdbfd45379cbba074f92b6 100644 --- a/spec/lib/gitlab/bitbucket_import/client_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/client_spec.rb @@ -34,18 +34,32 @@ describe Gitlab::BitbucketImport::Client, lib: true do it 'retrieves issues over a number of pages' do stub_request(:get, "https://bitbucket.org/api/1.0/repositories/#{project_id}/issues?limit=50&sort=utc_created_on&start=0"). - to_return(status: 200, - body: first_sample_data.to_json, - headers: {}) + to_return(status: 200, + body: first_sample_data.to_json, + headers: {}) stub_request(:get, "https://bitbucket.org/api/1.0/repositories/#{project_id}/issues?limit=50&sort=utc_created_on&start=50"). - to_return(status: 200, - body: second_sample_data.to_json, - headers: {}) + to_return(status: 200, + body: second_sample_data.to_json, + headers: {}) issues = client.issues(project_id) expect(issues.count).to eq(95) end end + + context 'project import' do + it 'calls .from_project with no errors' do + project = create(:empty_project) + project.create_or_update_import_data(credentials: + { user: "git", + password: nil, + bb_session: { bitbucket_access_token: "test", + bitbucket_access_token_secret: "test" } }) + project.import_url = "ssh://git@bitbucket.org/test/test.git" + + expect { described_class.from_project(project) }.to_not raise_error + end + end end diff --git a/spec/lib/gitlab/github_import/comment_formatter_spec.rb b/spec/lib/gitlab/github_import/comment_formatter_spec.rb index a324a82e69fc5d9afc64131aa3c2eacbbc43cd2e..55e86d4ceac88fcc1e242d779cbe52db8023864a 100644 --- a/spec/lib/gitlab/github_import/comment_formatter_spec.rb +++ b/spec/lib/gitlab/github_import/comment_formatter_spec.rb @@ -2,23 +2,25 @@ require 'spec_helper' describe Gitlab::GithubImport::CommentFormatter, lib: true do let(:project) { create(:project) } - let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') } + let(:octocat) { double(id: 123456, login: 'octocat') } let(:created_at) { DateTime.strptime('2013-04-10T20:09:31Z') } let(:updated_at) { DateTime.strptime('2014-03-03T18:58:10Z') } - let(:base_data) do + let(:base) do { body: "I'm having a problem with this.", user: octocat, + commit_id: nil, + diff_hunk: nil, created_at: created_at, updated_at: updated_at } end - subject(:comment) { described_class.new(project, raw_data)} + subject(:comment) { described_class.new(project, raw)} describe '#attributes' do context 'when do not reference a portion of the diff' do - let(:raw_data) { OpenStruct.new(base_data) } + let(:raw) { double(base) } it 'returns formatted attributes' do expected = { @@ -36,24 +38,23 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do end context 'when on a portion of the diff' do - let(:diff_data) do + let(:diff) do { body: 'Great stuff', commit_id: '6dcb09b5b57875f334f61aebed695e2e4193db5e', - diff_hunk: '@@ -16,33 +16,40 @@ public class Connection : IConnection...', - path: 'file1.txt', - position: 1 + diff_hunk: "@@ -1,5 +1,9 @@\n class User\n def name\n- 'John Doe'\n+ 'Jane Doe'", + path: 'file1.txt' } end - let(:raw_data) { OpenStruct.new(base_data.merge(diff_data)) } + let(:raw) { double(base.merge(diff)) } it 'returns formatted attributes' do expected = { project: project, note: "*Created by: octocat*\n\nGreat stuff", commit_id: '6dcb09b5b57875f334f61aebed695e2e4193db5e', - line_code: 'ce1be0ff4065a6e9415095c95f25f47a633cef2b_0_1', + line_code: 'ce1be0ff4065a6e9415095c95f25f47a633cef2b_4_3', author_id: project.creator_id, created_at: created_at, updated_at: updated_at @@ -64,15 +65,10 @@ describe Gitlab::GithubImport::CommentFormatter, lib: true do end context 'when author is a GitLab user' do - let(:raw_data) { OpenStruct.new(base_data.merge(user: octocat)) } + let(:raw) { double(base.merge(user: octocat)) } - it 'returns project#creator_id as author_id when is not a GitLab user' do - expect(comment.attributes.fetch(:author_id)).to eq project.creator_id - end - - it 'returns GitLab user id as author_id when is a GitLab user' do + it 'returns GitLab user id as author_id' do gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') - expect(comment.attributes.fetch(:author_id)).to eq gl_user.id end end diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index 04bc2dcfb16a80c9b8ebc062262b6c9aebdf465f..37a27d73aab89207b5ed58dc56cdf21b386272bb 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 31b2c90122d94e772f166505ad802b4ddc6e0fec..e771f35811ec004e3e6e4af8da371addf625cf99 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 88cd624877a8e18f1e7de718ab9705bd59fd8e5f..60364df20154469aabebe1d5a036330c5a3c9647 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 7c23c2efccd8e2f97c57c25e750c98db22bad252..236df8f047df335a682329594893cde6110c1311 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 0000000000000000000000000000000000000000..3e6da42803b800822d640ae8c7f163fb1a751d5b --- /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 0000000000000000000000000000000000000000..ff976f8ec5932ff613ae4f51fc9e395b566c9155 --- /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 a2cf68a9e387c037dda62ce3ab5b10850f5d55d3..3a8e67438fc308f98b4a03b5f5aac9757a48907a 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 0000000000000000000000000000000000000000..e6f78898c822c198d10e6634f7b62e101e08e50e --- /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 similarity index 78% rename from spec/models/external_wiki_service_spec.rb rename to spec/models/project_services/external_wiki_service_spec.rb index d37978720bf9cd945fe10a5bcad1533d544649bc..5fe5ea7d2dfc518e677f4a39e5ad363e6d7da050 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 ff7fbcaa00443d634bbe83344254e0eb31f924a0..b7e627e6518cb9e6584e5ed3dd022cd1f9189c32 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 ecb3ccb16732b4a5c4d7ae8f70983edf1c96f09c..a08f1ac229f1ed8c090bfbad65197b1bac684f3f 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 3518dbd172863d435f4a34489a7fbfb63c225b74..7a1f106d6e3a3a7b4d2065c790a2b998220d93a3 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 d878162a220f5072263968ff3d7ac20beb3a9204..6fb5cad50119f181d00953ccd33ccf7d1209b8b1 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 b783b1a576ec8dc931f4aa356c11b338d755ab22..4ee022a51710ec490efbc85eb824de543b1e9df1 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 2f8193170aedf3009a9377879a2b4da0c7c11c08..5309cfb99fff5154d442df92a2edf79666d61ba3 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 0000000000000000000000000000000000000000..f37edd4d970967086a38870a2aff1c5fb809cc10 --- /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 96039f9491bc49619de645fe3ccc8835296b0283..555d9757b47a7bf55ab7979f7fbafdecdb6b5f3b 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 0000000000000000000000000000000000000000..7d14f6e82806f6466dad3a06c1da7e2865f09358 --- /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 478d59be08ba867fd96c7fd11e1c6f1ce5ffb99d..a97b7560137fdaec9860916a2926690bf2b71341 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 bc7423cee6986a39f9cd1d5103799b0ecf3484fe..ad24b895170cb2531cb55937b0456c7e4779d5cd 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 e33c7d62ff42ecfd8b5bf262dd43b18aa9503dd7..5b1cf71337e552a525e14c7a7b75cfd5b9b4c242 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 397bb5a80282c9bfb582ff9b6a5f6193af955fe1..34a13f9b5c9fff85f5f25a33fa604faafccb728b 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -561,7 +561,7 @@ describe Repository, models: true do end describe :skip_merged_commit do - subject { repository.commits(Gitlab::Git::BRANCH_REF_PREFIX + "'test'", nil, 100, 0, true).map{ |k| k.id } } + subject { repository.commits(Gitlab::Git::BRANCH_REF_PREFIX + "'test'", limit: 100, skip_merges: true).map{ |k| k.id } } it { is_expected.not_to include('e56497bb5f03a90a51293fc6d516788730953899') } end @@ -858,13 +858,30 @@ describe Repository, models: true do end describe '#add_tag' do - it 'adds a tag' do - expect(repository).to receive(:before_push_tag) + context 'with a valid target' do + let(:user) { build_stubbed(:user) } - expect_any_instance_of(Gitlab::Shell).to receive(:add_tag). - with(repository.path_with_namespace, '8.5', 'master', 'foo') + it 'creates the tag using rugged' do + expect(repository.rugged.tags).to receive(:create). + with('8.5', repository.commit('master').id, + hash_including(message: 'foo', + tagger: hash_including(name: user.name, email: user.email))). + and_call_original - repository.add_tag('8.5', 'master', 'foo') + repository.add_tag(user, '8.5', 'master', 'foo') + end + + it 'returns a Gitlab::Git::Tag object' do + tag = repository.add_tag(user, '8.5', 'master', 'foo') + + expect(tag).to be_a(Gitlab::Git::Tag) + end + end + + context 'with an invalid target' do + it 'returns false' do + expect(repository.add_tag(user, '8.5', 'bar', 'foo')).to be false + end end end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index e28998d51b5d41011bf30a43cbb1d87782cb92d0..cb82ca7802daf94d263e724726247404fe13f48e 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -32,6 +32,41 @@ describe API::API, api: true do expect(response.status).to eq(401) end end + + context "since optional parameter" do + it "should return project commits since provided parameter" do + commits = project.repository.commits("master") + since = commits.second.created_at + + get api("/projects/#{project.id}/repository/commits?since=#{since.utc.iso8601}", user) + + expect(json_response.size).to eq 2 + expect(json_response.first["id"]).to eq(commits.first.id) + expect(json_response.second["id"]).to eq(commits.second.id) + end + end + + context "until optional parameter" do + it "should return project commits until provided parameter" do + commits = project.repository.commits("master") + before = commits.second.created_at + + get api("/projects/#{project.id}/repository/commits?until=#{before.utc.iso8601}", user) + + expect(json_response.size).to eq(commits.size - 1) + expect(json_response.first["id"]).to eq(commits.second.id) + expect(json_response.second["id"]).to eq(commits.third.id) + end + end + + context "invalid xmlschema date parameters" do + it "should return an invalid parameter error message" do + get api("/projects/#{project.id}/repository/commits?since=invalid-date", user) + + expect(response.status).to eq(400) + expect(json_response['message']).to include "\"since\" must be a timestamp in ISO 8601 format" + end + end end describe "GET /projects:id/repository/commits/:sha" do diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb index 344f0fe0b7fcbebae65249a9288656b83d9a7eee..241995041bb113828eac54c99efb9dd4e512f578 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 ec9eda0a2edd29966c9766eae2ce3d4a95c295a1..49091fc0f49d416bcfa294a4ea22056141ec076f 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 3722ddf5a33f1d867cba5551999b169ebb0d045f..9706d060cfaea22c06f7b9d5cba92a0aa1562421 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 fccd08bd6dac024f62463525b277d72591ec73f2..66193eac051e1592ec0e00d689688cecccbbbdfb 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/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index edcb2bedbf793131442a0b26070b5582397939c9..12e170b232f23d7b628cad7ad1c24b8ef07600c3 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -147,7 +147,7 @@ describe API::API, api: true do tag_name: 'v8.0.0', ref: 'master' expect(response.status).to eq(400) - expect(json_response['message']).to eq('Tag already exists') + expect(json_response['message']).to eq('Tag v8.0.0 already exists') end it 'should return 400 if ref name is invalid' do @@ -155,7 +155,7 @@ describe API::API, api: true do tag_name: 'mytag', ref: 'foo' expect(response.status).to eq(400) - expect(json_response['message']).to eq('Invalid reference name') + expect(json_response['message']).to eq('Target foo is invalid') end end diff --git a/spec/services/create_tag_service_spec.rb b/spec/services/create_tag_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..91f9e663b66d367d8e589c64ddb51ad30b9d622d --- /dev/null +++ b/spec/services/create_tag_service_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe CreateTagService, services: true do + let(:project) { create(:project) } + let(:repository) { project.repository } + let(:user) { create(:user) } + let(:service) { described_class.new(project, user) } + + describe '#execute' do + it 'creates the tag and returns success' do + response = service.execute('v42.42.42', 'master', 'Foo') + + expect(response[:status]).to eq(:success) + expect(response[:tag]).to be_a Gitlab::Git::Tag + expect(response[:tag].name).to eq('v42.42.42') + end + + context 'when target is invalid' do + it 'returns an error' do + response = service.execute('v1.1.0', 'foo', 'Foo') + + expect(response).to eq(status: :error, + message: 'Target foo is invalid') + end + end + + context 'when tag already exists' do + it 'returns an error' do + expect(repository).to receive(:add_tag). + with(user, 'v1.1.0', 'master', 'Foo'). + and_raise(Rugged::TagError) + + response = service.execute('v1.1.0', 'master', 'Foo') + + expect(response).to eq(status: :error, + message: 'Tag v1.1.0 already exists') + end + end + + context 'when pre-receive hook fails' do + it 'returns an error' do + expect(repository).to receive(:add_tag). + with(user, 'v1.1.0', 'master', 'Foo'). + and_raise(GitHooksService::PreReceiveError) + + response = service.execute('v1.1.0', 'master', 'Foo') + + expect(response).to eq(status: :error, + message: 'Tag creation was rejected by Git hook') + end + end + end +end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 2a5e4ac3ec476d0523c7ffdd9056d17dc2fc6f86..c15e26189a518a7a03d204b12779c519ca649495 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -7,10 +7,11 @@ describe Issues::MoveService, services: true do let(:description) { 'Some issue description' } let(:old_project) { create(:project) } let(:new_project) { create(:project) } + let(:milestone1) { create(:milestone, project_id: old_project.id, title: 'v9.0') } let(:old_issue) do create(:issue, title: title, description: description, - project: old_project, author: author) + project: old_project, author: author, milestone: milestone1) end let(:move_service) do @@ -21,11 +22,24 @@ describe Issues::MoveService, services: true do before do old_project.team << [user, :reporter] new_project.team << [user, :reporter] + + ['label1', 'label2'].each do |label| + old_issue.labels << create(:label, + project_id: old_project.id, + title: label) + end + + new_project.labels << create(:label, title: 'label1') + new_project.labels << create(:label, title: 'label2') end end describe '#execute' do shared_context 'issue move executed' do + let!(:milestone2) do + create(:milestone, project_id: new_project.id, title: 'v9.0') + end + let!(:new_issue) { move_service.execute(old_issue, new_project) } end @@ -39,6 +53,23 @@ describe Issues::MoveService, services: true do expect(new_issue.project).to eq new_project end + it 'assigns milestone to new issue' do + expect(new_issue.reload.milestone.title).to eq 'v9.0' + expect(new_issue.reload.milestone).to eq(milestone2) + end + + it 'assign labels to new issue' do + expected_label_titles = new_issue.reload.labels.map(&:title) + expect(expected_label_titles).to include 'label1' + expect(expected_label_titles).to include 'label2' + expect(expected_label_titles.size).to eq 2 + + new_issue.labels.each do |label| + expect(new_project.labels).to include(label) + expect(old_project.labels).not_to include(label) + end + end + it 'rewrites issue title' do expect(new_issue.title).to eq title end @@ -72,11 +103,6 @@ describe Issues::MoveService, services: true do expect(new_issue.author).to eq author end - it 'removes data that is invalid in new context' do - expect(new_issue.milestone).to be_nil - expect(new_issue.labels).to be_empty - end - it 'creates a new internal id for issue' do expect(new_issue.iid).to be 1 end diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..782d74ec5ecf3f331b4c8a451877b8f2f83e6962 --- /dev/null +++ b/spec/services/merge_requests/build_service_spec.rb @@ -0,0 +1,181 @@ +require 'spec_helper' + +describe MergeRequests::BuildService, services: true do + include RepoHelpers + + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:issue_confidential) { false } + let(:issue) { create(:issue, project: project, title: 'A bug', confidential: issue_confidential) } + let(:description) { nil } + let(:source_branch) { 'feature-branch' } + let(:target_branch) { 'master' } + let(:merge_request) { service.execute } + let(:compare) { double(:compare, commits: commits) } + let(:commit_1) { double(:commit_1, safe_message: "Initial commit\n\nCreate the app") } + let(:commit_2) { double(:commit_2, safe_message: 'This is a bad commit message!') } + let(:commits) { nil } + + let(:service) do + MergeRequests::BuildService.new(project, user, + description: description, + source_branch: source_branch, + target_branch: target_branch) + end + + before do + allow(CompareService).to receive_message_chain(:new, :execute).and_return(compare) + end + + describe 'execute' do + context 'missing source branch' do + let(:source_branch) { '' } + + it 'forbids the merge request from being created' do + expect(merge_request.can_be_created).to eq(false) + end + + it 'adds an error message to the merge request' do + expect(merge_request.errors).to contain_exactly('You must select source and target branch') + end + end + + context 'missing target branch' do + let(:target_branch) { '' } + + it 'forbids the merge request from being created' do + expect(merge_request.can_be_created).to eq(false) + end + + it 'adds an error message to the merge request' do + expect(merge_request.errors).to contain_exactly('You must select source and target branch') + end + end + + context 'no commits in the diff' do + let(:commits) { [] } + + it 'forbids the merge request from being created' do + expect(merge_request.can_be_created).to eq(false) + end + end + + context 'one commit in the diff' do + let(:commits) { [commit_1] } + + it 'allows the merge request to be created' do + expect(merge_request.can_be_created).to eq(true) + end + + it 'uses the title of the commit as the title of the merge request' do + expect(merge_request.title).to eq(commit_1.safe_message.split("\n").first) + end + + it 'uses the description of the commit as the description of the merge request' do + expect(merge_request.description).to eq(commit_1.safe_message.split(/\n+/, 2).last) + end + + context 'merge request already has a description set' do + let(:description) { 'Merge request description' } + + it 'keeps the description from the initial params' do + expect(merge_request.description).to eq(description) + end + end + + context 'commit has no description' do + let(:commits) { [commit_2] } + + it 'uses the title of the commit as the title of the merge request' do + expect(merge_request.title).to eq(commit_2.safe_message) + end + + it 'sets the description to nil' do + expect(merge_request.description).to be_nil + end + end + + context 'branch starts with issue IID followed by a hyphen' do + let(:source_branch) { "#{issue.iid}-fix-issue" } + + it 'appends "Closes #$issue-iid" to the description' do + expect(merge_request.description).to eq("#{commit_1.safe_message.split(/\n+/, 2).last}\nCloses ##{issue.iid}") + end + + context 'merge request already has a description set' do + let(:description) { 'Merge request description' } + + it 'appends "Closes #$issue-iid" to the description' do + expect(merge_request.description).to eq("#{description}\nCloses ##{issue.iid}") + end + end + + context 'commit has no description' do + let(:commits) { [commit_2] } + + it 'sets the description to "Closes #$issue-iid"' do + expect(merge_request.description).to eq("Closes ##{issue.iid}") + end + end + end + end + + context 'more than one commit in the diff' do + let(:commits) { [commit_1, commit_2] } + + it 'allows the merge request to be created' do + expect(merge_request.can_be_created).to eq(true) + end + + it 'uses the title of the branch as the merge request title' do + expect(merge_request.title).to eq('Feature branch') + end + + it 'does not add a description' do + expect(merge_request.description).to be_nil + end + + context 'merge request already has a description set' do + let(:description) { 'Merge request description' } + + it 'keeps the description from the initial params' do + expect(merge_request.description).to eq(description) + end + end + + context 'branch starts with GitLab issue IID followed by a hyphen' do + let(:source_branch) { "#{issue.iid}-fix-issue" } + + it 'sets the title to: Resolves "$issue-title"' do + expect(merge_request.title).to eq("Resolve \"#{issue.title}\"") + end + + context 'issue does not exist' do + let(:source_branch) { "#{issue.iid.succ}-fix-issue" } + + it 'uses the title of the branch as the merge request title' do + expect(merge_request.title).to eq("#{issue.iid.succ} fix issue") + end + end + + context 'issue is confidential' do + let(:issue_confidential) { true } + + it 'uses the title of the branch as the merge request title' do + expect(merge_request.title).to eq("#{issue.iid} fix issue") + end + end + end + + context 'branch starts with external issue IID followed by a hyphen' do + let(:source_branch) { '12345-fix-issue' } + + before { allow(project).to receive(:default_issues_tracker?).and_return(false) } + + it 'sets the title to: Resolves External Issue $issue-iid' do + expect(merge_request.title).to eq('Resolve External Issue 12345') + end + end + end + end +end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index d7c72dc08118672c76097fdaa79e59ad27b7330f..4bbc4ddc3ab2d235583dfb5139dade32118eab99 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/spec_helper.rb b/spec/spec_helper.rb index 596d607f2a169e77d9f92536f7442fc9b96ca221..576d16e7ea33fe7d2a652f709ca3e67ae4b1e100 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -51,10 +51,4 @@ FactoryGirl::SyntaxRunner.class_eval do include RSpec::Mocks::ExampleMethods end -# Work around a Rails 4.2.5.1 issue -# See https://github.com/rspec/rspec-rails/issues/1532 -RSpec::Rails::ViewRendering::EmptyTemplatePathSetDecorator.class_eval do - alias_method :find_all_anywhere, :find_all -end - ActiveRecord::Migration.maintain_test_schema! 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 0000000000000000000000000000000000000000..b6d7436c3609efa3d740c237ed4fa0704250ccd8 --- /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/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb index 087e4c667d8caed39073712d34b4c3d6dd88ef1b..5a03bb77ebdee994f83c9891d247d66ed256a02f 100644 --- a/spec/workers/repository_check/single_repository_worker_spec.rb +++ b/spec/workers/repository_check/single_repository_worker_spec.rb @@ -12,7 +12,7 @@ describe RepositoryCheck::SingleRepositoryWorker do subject.perform(project.id) expect(project.reload.last_repository_check_failed).to eq(false) - destroy_wiki(project) + break_wiki(project) subject.perform(project.id) expect(project.reload.last_repository_check_failed).to eq(true) @@ -20,15 +20,38 @@ describe RepositoryCheck::SingleRepositoryWorker do it 'skips wikis when disabled' do project = create(:project_empty_repo, wiki_enabled: false) - # Make sure the test would fail if it checked the wiki repo - destroy_wiki(project) + # Make sure the test would fail if the wiki repo was checked + break_wiki(project) subject.perform(project.id) expect(project.reload.last_repository_check_failed).to eq(false) end - def destroy_wiki(project) - FileUtils.rm_rf(project.wiki.repository.path_to_repo) + it 'creates missing wikis' do + project = create(:project_empty_repo, wiki_enabled: true) + FileUtils.rm_rf(wiki_path(project)) + + subject.perform(project.id) + + expect(project.reload.last_repository_check_failed).to eq(false) + end + + it 'does not create a wiki if the main repo does not exist at all' do + project = create(:project_empty_repo) + FileUtils.rm_rf(project.repository.path_to_repo) + FileUtils.rm_rf(wiki_path(project)) + + subject.perform(project.id) + + expect(File.exist?(wiki_path(project))).to eq(false) + end + + def break_wiki(project) + FileUtils.rm_rf(wiki_path(project) + '/objects') + end + + def wiki_path(project) + project.wiki.repository.path_to_repo end end