diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 141e7ba41dec9c7c570747065d23aa8fbaed432f..749249a2eba9abb15ce88358d407152b50e76b7b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,19 @@ -# This file is generated by GitLab CI +image: "ruby:2.1" + +services: + - mysql:latest + - redis:latest + +cache: + key: "ruby21" + paths: + - vendor + +variables: + MYSQL_ALLOW_EMPTY_PASSWORD: "1" + before_script: - - ./scripts/prepare_build.sh + - source ./scripts/prepare_build.sh - ruby -v - which ruby - gem install bundler --no-ri --no-rdoc @@ -8,7 +21,7 @@ before_script: - touch log/application.log - touch log/test.log - bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" - - bundle exec rake db:create RAILS_ENV=test + - RAILS_ENV=test bundle exec rake db:drop db:create db:schema:load db:migrate spec:feature: script: diff --git a/CHANGELOG b/CHANGELOG index ca09d9c09f398b59caf5dafd82ff39ad740438e1..4f263be1fe92e63506ebf9d80627ff42e89bbf89 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,15 +1,57 @@ Please view this file on the master branch, on stable branches it's out of date. -v 8.3.0 (unreleased) +v 8.2.6 + - Prevent unauthorized access to other projects build traces + - Forbid scripting for wiki files + +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) + - Update documentation for "Guest" permissions + - Properly convert Emoji-only comments into Award Emojis + - Enable devise paranoid mode to prevent user enumeration attack + - Webhook payload has added, modified, and removed properties for each commit + - Fix 500 error when creating a merge request that removes a submodule + +v 8.2.2 + - Fix 404 in redirection after removing a project (Stan Hu) + - Ensure cached application settings are refreshed at startup (Stan Hu) + - Fix Error 500 when viewing user's personal projects from admin page (Stan Hu) + - Fix: Raw private snippets access workflow + - Prevent "413 Request entity too large" errors when pushing large files with LFS + - Ensure GitLab fires custom update hooks after commit via UI + +v 8.2.1 + - Forcefully update builds that didn't want to update with state machine + - Fix: saving GitLabCiService as Admin Template v 8.2.0 - - Remove CSS property preventing hard tabs from rendering in Chromium 45 (Stan Hu) + - Improved performance of finding projects and groups in various places + - Improved performance of rendering user profile pages and Atom feeds + - Expose build artifacts path as config option + - Fix grouping of contributors by email in graph. + - Improved performance of finding issues with/without labels - Fix Drone CI service template not saving properly (Stan Hu) - Fix avatars not showing in Atom feeds and project issues when Gravatar disabled (Stan Hu) - Added a GitLab specific profiling tool called "Sherlock" (see GitLab CE merge request #1749) - Upgrade gitlab_git to 7.2.20 and rugged to 0.23.3 (Stan Hu) - Improved performance of finding users by one of their Email addresses - Add allow_failure field to commit status API (Stan Hu) + - Commits without .gitlab-ci.yml are marked as skipped + - Save detailed error when YAML syntax is invalid + - Since GitLab CI is enabled by default, remove enabling it by pushing .gitlab-ci.yml + - Added build artifacts - Improved performance of replacing references in comments - Show last project commit to default branch on project home page - Highlight comment based on anchor in URL @@ -27,6 +69,7 @@ v 8.2.0 - Allow to define cache in `.gitlab-ci.yml` - Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu) - Remove deprecated CI events from project settings page + - Improve personal snippet access workflow (Douglas Alexandre) - [API] Add ability to fetch the commit ID of the last commit that actually touched a file - Fix omniauth documentation setting for omnibus configuration (Jon Cairns) - Add "New file" link to dropdown on project page @@ -34,6 +77,7 @@ v 8.2.0 - Add "added", "modified" and "removed" properties to commit object in webhook - Rename "Back to" links to "Go to" because its not always a case it point to place user come from - Allow groups to appear in the search results if the group owner allows it + - Add email notification to former assignee upon unassignment (Adam Lieskovský) - New design for project graphs page - Remove deprecated dumped yaml file generated from previous job definitions - Fix incoming email config defaults @@ -47,6 +91,9 @@ v 8.2.0 - Fix trailing whitespace issue in merge request/issue title - Fix bug when milestone/label filter was empty for dashboard issues page - Add ability to create milestone in group projects from single form + - Add option to create merge request when editing/creating a file (Dirceu Tiegs) + - Prevent the last owner of a group from being able to delete themselves by 'adding' themselves as a master (James Lopez) + - Add Award Emoji to issue and merge request pages v 8.1.4 - Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu) @@ -116,7 +163,6 @@ v 8.1.0 - Show CI status on Your projects page and Starred projects page - Remove "Continuous Integration" page from dashboard - Add notes and SSL verification entries to hook APIs (Ben Boeckel) - - Added build artifacts - Fix grammar in admin area "labels" .nothing-here-block when no labels exist. - Move CI runners page to project settings area - Move CI variables page to project settings area diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 338a5b5d8fec491b97978114dc35e36348fa56a7..743af5e1251c4fc79e10f34ef7c9f24b1367ab72 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -2.6.6 +2.6.8 diff --git a/GITLAB_WORKHORSE b/GITLAB_WORKHORSE deleted file mode 100644 index 267577d47e497a0630bc454b3f74c4fd9a10ced4..0000000000000000000000000000000000000000 --- a/GITLAB_WORKHORSE +++ /dev/null @@ -1 +0,0 @@ -0.4.1 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 9e11b32fcaa96816319e5d0dcff9fb2873f04061..2b7c5ae01848a77d95e2792eb83ab605c9aed91a 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -0.3.1 +0.4.2 diff --git a/Gemfile b/Gemfile index 8a19885bcb199b49e5cfb081e1b43476d924e50a..deab4a2f05ec7e00e05b2d95e671ec2b6f8f747b 100644 --- a/Gemfile +++ b/Gemfile @@ -170,6 +170,7 @@ gem "underscore-rails", "~> 1.4.4" # Sanitize user input gem "sanitize", '~> 2.0' +gem 'babosa', '~> 1.0.2' # Protect against bruteforcing gem "rack-attack", '~> 4.3.0' diff --git a/Gemfile.lock b/Gemfile.lock index 99cdc2a50aee35acf5dbc4b2dbc35bf62cc220a1..d41f5522fbffb009c805ac7ada39a66678917818 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -65,6 +65,7 @@ GEM descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) + babosa (1.0.2) bcrypt (3.1.10) benchmark-ips (2.3.0) better_errors (1.0.1) @@ -795,6 +796,7 @@ DEPENDENCIES asciidoctor (~> 1.5.2) attr_encrypted (~> 1.3.4) awesome_print (~> 1.2.0) + babosa (~> 1.0.2) benchmark-ips better_errors (~> 1.0.1) binding_of_caller (~> 0.7.2) diff --git a/README.md b/README.md index 52e2d9776208e95b7d5c11bcfc7f2fe63fcab34c..d01c5f3bfc8f3f1f320f68e4f82c1c80a3069e4c 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ GitLab is a Ruby on Rails application that runs on the following software: - Ubuntu/Debian/CentOS/RHEL - Ruby (MRI) 2.1 -- Git 1.7.10+ +- Git 2.7.4+ - Redis 2.4+ - MySQL or PostgreSQL diff --git a/VERSION b/VERSION index a2264f05f500e20edac8133bf7b800dc35bc37bd..d00d9ba64a33dc337e22b454df8cb933597701a2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.1.0.pre +8.2.6 diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee new file mode 100644 index 0000000000000000000000000000000000000000..96fd8f8773ee43e4c4f67fea63d07c5f677170e8 --- /dev/null +++ b/app/assets/javascripts/awards_handler.coffee @@ -0,0 +1,96 @@ +class @AwardsHandler + constructor: (@post_emoji_url, @noteable_type, @noteable_id) -> + + addAward: (emoji) -> + @postEmoji emoji, => + @addAwardToEmojiBar(emoji) + + addAwardToEmojiBar: (emoji, custom_path = '') -> + if @exist(emoji) + if @isActive(emoji) + @decrementCounter(emoji) + else + counter = @findEmojiIcon(emoji).siblings(".counter") + counter.text(parseInt(counter.text()) + 1) + counter.parent().addClass("active") + @addMeToAuthorList(emoji) + else + @createEmoji(emoji, custom_path) + + exist: (emoji) -> + @findEmojiIcon(emoji).length > 0 + + isActive: (emoji) -> + @findEmojiIcon(emoji).parent().hasClass("active") + + decrementCounter: (emoji) -> + counter = @findEmojiIcon(emoji).siblings(".counter") + + if parseInt(counter.text()) > 1 + counter.text(parseInt(counter.text()) - 1) + counter.parent().removeClass("active") + @removeMeFromAuthorList(emoji) + else + award = counter.parent() + award.tooltip("destroy") + award.remove() + + removeMeFromAuthorList: (emoji) -> + award_block = @findEmojiIcon(emoji).parent() + authors = award_block.attr("data-original-title").split(", ") + authors = _.without(authors, "me").join(", ") + award_block.attr("title", authors) + @resetTooltip(award_block) + + addMeToAuthorList: (emoji) -> + award_block = @findEmojiIcon(emoji).parent() + authors = award_block.attr("data-original-title").split(", ") + authors.push("me") + award_block.attr("title", authors.join(", ")) + @resetTooltip(award_block) + + resetTooltip: (award) -> + award.tooltip("destroy") + + # "destroy" call is asynchronous, this is why we need to set timeout. + setTimeout (-> + award.tooltip() + ), 200 + + + createEmoji: (emoji, custom_path) -> + nodes = [] + nodes.push("
") + nodes.push("
") + nodes.push(@getImage(emoji, custom_path)) + nodes.push("
") + nodes.push("
1") + nodes.push("
") + + $(".awards-controls").before(nodes.join("\n")) + + $(".award").tooltip() + + getImage: (emoji, custom_path) -> + if custom_path + $("").attr({src: custom_path, width: 20, height: 20}).wrap("
").parent().html() + else + $("li[data-emoji='" + emoji + "']").html() + + + postEmoji: (emoji, callback) -> + $.post @post_emoji_url, { note: { + note: ":" + emoji + ":" + noteable_type: @noteable_type + noteable_id: @noteable_id + }},(data) -> + if data.ok + callback.call() + + findEmojiIcon: (emoji) -> + $(".icon[data-emoji='" + emoji + "']") + + scrollToAwards: -> + $('body, html').animate({ + scrollTop: $('.awards').offset().top - 80 + }, 200) diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee index 5b604adbbb1bca76236caaee1f5395f6cb7f4059..195f8b11e5dbac896379cd30d13f3caa3532e130 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee +++ b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee @@ -23,18 +23,6 @@ class @BlobFileDropzone init: -> this.on 'addedfile', (file) -> $('.dropzone-alerts').html('').hide() - commit_message = form.find('#commit_message')[0] - - if /^Upload/.test(commit_message.placeholder) - commit_message.placeholder = 'Upload ' + file.name - - return - - this.on 'removedfile', (file) -> - commit_message = form.find('#commit_message')[0] - - if /^Upload/.test(commit_message.placeholder) - commit_message.placeholder = 'Upload new file' return @@ -47,8 +35,9 @@ class @BlobFileDropzone return this.on 'sending', (file, xhr, formData) -> - formData.append('new_branch', form.find('#new_branch').val()) - formData.append('commit_message', form.find('#commit_message').val()) + formData.append('new_branch', form.find('.js-new-branch').val()) + formData.append('create_merge_request', form.find('.js-create-merge-request').val()) + formData.append('commit_message', form.find('.js-commit-message').val()) return # Override behavior of adding error underneath preview diff --git a/app/assets/javascripts/copy_to_clipboard.js.coffee b/app/assets/javascripts/copy_to_clipboard.js.coffee index ec4b80cca6f51e5ac62e7061602e4c93d848a28b..9c68c5cc1bcdf377edf3a958c7dc5ffc2dc50554 100644 --- a/app/assets/javascripts/copy_to_clipboard.js.coffee +++ b/app/assets/javascripts/copy_to_clipboard.js.coffee @@ -9,13 +9,24 @@ $ -> clipboard.on 'success', (e) -> $(e.trigger). tooltip(trigger: 'manual', placement: 'auto bottom', title: 'Copied!'). - tooltip('show') + tooltip('show'). + one('mouseleave', -> $(this).tooltip('hide')) # Clear the selection and blur the trigger so it loses its border e.clearSelection() $(e.trigger).blur() - # Manually hide the tooltip after 1 second - setTimeout(-> - $(e.trigger).tooltip('hide') - , 1000) + # Safari doesn't support `execCommand`, so instead we inform the user to + # copy manually. + # + # See http://clipboardjs.com/#browser-support + clipboard.on 'error', (e) -> + if /Mac/i.test(navigator.userAgent) + title = "Press ⌘-C to copy" + else + title = "Press Ctrl-C to copy" + + $(e.trigger). + tooltip(trigger: 'manual', placement: 'auto bottom', html: true, title: title). + tooltip('show'). + one('mouseleave', -> $(this).tooltip('hide')) diff --git a/app/assets/javascripts/flash.js.coffee b/app/assets/javascripts/flash.js.coffee index b39ab0c447580cbd7f5ef80f38f395da8f1bba1b..9b59d4e57f7ccf8e22ea9257b736c7b432512bdc 100644 --- a/app/assets/javascripts/flash.js.coffee +++ b/app/assets/javascripts/flash.js.coffee @@ -1,12 +1,16 @@ class @Flash constructor: (message, type)-> - flash = $(".flash-container") - flash.html("") + @flash = $(".flash-container") + @flash.html("") - $('
', + innerDiv = $('
', class: "flash-#{type}", text: message - ).appendTo(".flash-container") + ) + innerDiv.appendTo(".flash-container") - flash.click -> $(@).fadeOut() - flash.show() + @flash.click -> $(@).fadeOut() + @flash.show() + + pin: -> + @flash.addClass('flash-pinned flash-raised') diff --git a/app/assets/javascripts/new_commit_form.js.coffee b/app/assets/javascripts/new_commit_form.js.coffee new file mode 100644 index 0000000000000000000000000000000000000000..2e561dea3e1eafe8a52f0bba807d36d8040d2fec --- /dev/null +++ b/app/assets/javascripts/new_commit_form.js.coffee @@ -0,0 +1,21 @@ +class @NewCommitForm + constructor: (form) -> + @newBranch = form.find('.js-new-branch') + @originalBranch = form.find('.js-original-branch') + @createMergeRequest = form.find('.js-create-merge-request') + @createMergeRequestFormGroup = form.find('.js-create-merge-request-form-group') + + @renderDestination() + @newBranch.keyup @renderDestination + + renderDestination: => + different = @newBranch.val() != @originalBranch.val() + + if different + @createMergeRequestFormGroup.show() + @createMergeRequest.prop('checked', true) unless @wasDifferent + else + @createMergeRequestFormGroup.hide() + @createMergeRequest.prop('checked', false) + + @wasDifferent = different diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index ea75c656bccb5025a2b556913d6b66aefda73c32..dd6cbcfc70b4da2abab338d218ea2251e77fcaf0 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -111,15 +111,25 @@ class @Notes Note: for rendering inline notes use renderDiscussionNote ### renderNote: (note) -> + unless note.valid + if note.award + flash = new Flash('You have already used this award emoji!', 'alert') + flash.pin() + return + # render note if it not present in loaded list # or skip if rendered - if @isNewNote(note) + if @isNewNote(note) && !note.award @note_ids.push(note.id) $('ul.main-notes-list'). append(note.html). syntaxHighlight() @initTaskList() + if note.award + awards_handler.addAwardToEmojiBar(note.note, note.emoji_path) + awards_handler.scrollToAwards() + ### Check if note does not exists on page ### @@ -255,7 +265,6 @@ class @Notes ### addNote: (xhr, note, status) => @renderNote(note) - @updateVotes() ### Called in response to the new note form being submitted @@ -473,9 +482,6 @@ class @Notes form = $(e.target).closest(".js-discussion-note-form") @removeDiscussionNoteForm(form) - updateVotes: -> - true - ### Called after an attachment file has been selected. diff --git a/app/assets/javascripts/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/stat_graph_contributors_util.js.coffee index cfe5508290f14255848d48c9c82b1d036c669442..f5584bcfe4b28c5fbd80486bc56168e86fc49a47 100644 --- a/app/assets/javascripts/stat_graph_contributors_util.js.coffee +++ b/app/assets/javascripts/stat_graph_contributors_util.js.coffee @@ -6,7 +6,7 @@ window.ContributorsStatGraphUtil = for entry in log @add_date(entry.date, total) unless total[entry.date]? - data = by_author[entry.author_name] #|| by_email[entry.author_email] + data = by_author[entry.author_name] || by_email[entry.author_email] data ?= @add_author(entry, by_author, by_email) @add_date(entry.date, data) unless data[entry.date] @@ -95,5 +95,4 @@ window.ContributorsStatGraphUtil = if date_range is null || date_range[0] <= new Date(date) <= date_range[1] true else - false - + false \ No newline at end of file diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index ddbacd7fd4100cc4e80b005ec2d1e8992a95e9ba..40f4beb1968e5fb3ba31d6187a33470afdbe4777 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -328,6 +328,10 @@ table { } } +.well { + margin-bottom: 0; +} + .search_box { @extend .well; text-align: center; diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index 82eb50ad4be068473bac0507f6fc1a1aa75750b0..1b723021d76d2d8ea998eba507a48e43e1a676a8 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -15,3 +15,13 @@ @extend .alert-danger; } } + +.flash-pinned { + position: fixed; + top: 80px; + width: 80%; +} + +.flash-raised { + z-index: 1000; +} diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 99d028d1228ee84bb88394051806b27943db204e..94f0ed761dfbd25cb8307e8ac909de54c3c601b3 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -172,7 +172,7 @@ } .panel-body { - form { + form, pre { margin: 0; } @@ -190,6 +190,10 @@ .btn { min-width: 124px; } + + .btn-clipboard { + min-width: 0px; + } } &.panel-small { diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss index fbd7c363de108f414325d5714496668c9827259f..a0e5f7554edecb2f5c1a1cffdab6e87adf36de01 100644 --- a/app/assets/stylesheets/pages/commit.scss +++ b/app/assets/stylesheets/pages/commit.scss @@ -56,6 +56,7 @@ li { padding: 3px 0px; + line-height: 20px; } } .new-file { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index abc27a19e3243e188245e4f9ad0901b2f1b71303..3a08ee70bc7b523e531b00d699fba8f029e838c9 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -101,3 +101,71 @@ background-color: $background-color; } } + +.awards { + @include clearfix; + line-height: 34px; + margin: 2px 0; + + .award { + @include border-radius(5px); + + border: 1px solid; + padding: 0px 10px; + float: left; + margin: 0 5px; + border-color: $border-color; + cursor: pointer; + + &.active { + border-color: $border-gray-light; + background-color: $gray-light; + + .counter { + font-weight: bold; + } + } + + .icon { + float: left; + margin-right: 10px; + } + + .counter { + float: left; + } + } + + .awards-controls { + margin-left: 10px; + float: left; + + .add-award { + font-size: 24px; + color: $gl-gray; + position: relative; + top: 2px; + + &:hover, + &:link { + text-decoration: none; + } + } + + .awards-menu { + padding: $gl-padding; + min-width: 214px; + + > li { + margin: 5px; + } + } + } + + .awards-menu{ + li { + float: left; + margin: 3px; + } + } +} 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/impersonation_controller.rb b/app/controllers/admin/impersonation_controller.rb deleted file mode 100644 index 0382402afa6432de55f9c14e4b0a13dd825f5e88..0000000000000000000000000000000000000000 --- a/app/controllers/admin/impersonation_controller.rb +++ /dev/null @@ -1,32 +0,0 @@ -class Admin::ImpersonationController < Admin::ApplicationController - skip_before_action :authenticate_admin!, only: :destroy - - before_action :user - before_action :authorize_impersonator! - - def create - session[:impersonator_id] = current_user.username - session[:impersonator_return_to] = request.env['HTTP_REFERER'] - - warden.set_user(user, scope: 'user') - - flash[:alert] = "You are impersonating #{user.username}." - - redirect_to root_path - 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..2db824c87ef823a451df0091f6a514350a73fae5 --- /dev/null +++ b/app/controllers/admin/impersonations_controller.rb @@ -0,0 +1,24 @@ +class Admin::ImpersonationsController < Admin::ApplicationController + skip_before_action :authenticate_admin! + before_action :authenticate_impersonator! + + def destroy + original_user = current_user + + warden.set_user(impersonator, scope: :user) + + session[:impersonator_id] = nil + + redirect_to admin_user_path(original_user) + end + + private + + def impersonator + @impersonator ||= User.find(session[:impersonator_id]) if session[:impersonator_id] + end + + def authenticate_impersonator! + render_404 unless impersonator && impersonator.is_admin? && !impersonator.blocked? + end +end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index d7c927d444cbc4b43d0d5bfe20f63bb8e2c5a5c8..740d4dd5a8f39d906778ffac50bbb4eaa476eca5 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -31,6 +31,22 @@ class Admin::UsersController < Admin::ApplicationController user end + def impersonate + if user.blocked? + flash[:alert] = "You cannot impersonate a blocked user" + + redirect_to admin_user_path(user) + else + session[:impersonator_id] = current_user.id + + warden.set_user(user, scope: :user) + + flash[:alert] = "You are now impersonating #{user.username}" + + redirect_to root_path + end + end + def block if user.block redirect_back_or_admin_user(notice: "Successfully blocked") diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb index 24dd1b5c93aba542c1edb9c4a96ce036886f7bd8..a4f6aff49b4556930bcbd6ec3fda964c466d376b 100644 --- a/app/controllers/ci/lints_controller.rb +++ b/app/controllers/ci/lints_controller.rb @@ -15,10 +15,10 @@ module Ci @builds = @config_processor.builds @status = true end - rescue Ci::GitlabCiYamlProcessor::ValidationError => e + rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e @error = e.message @status = false - rescue Exception + rescue @error = "Undefined error" @status = false end diff --git a/app/controllers/concerns/creates_merge_request_for_commit.rb b/app/controllers/concerns/creates_merge_request_for_commit.rb new file mode 100644 index 0000000000000000000000000000000000000000..c75278221585a95a18e591f63c1e7839727ace7a --- /dev/null +++ b/app/controllers/concerns/creates_merge_request_for_commit.rb @@ -0,0 +1,28 @@ +module CreatesMergeRequestForCommit + extend ActiveSupport::Concern + + def new_merge_request_path + if @project.forked? + target_project = @project.forked_from_project || @project + target_branch = target_project.repository.root_ref + else + target_project = @project + target_branch = @ref + end + + new_namespace_project_merge_request_path( + @project.namespace, + @project, + merge_request: { + source_project_id: @project.id, + target_project_id: target_project.id, + source_branch: @new_branch, + target_branch: target_branch + } + ) + end + + def create_merge_request? + params[:create_merge_request] && @new_branch != @ref + end +end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index b25957a06e2a82c192c11e5aa45343dbb2e28531..0e902c4bb4347958201b5430b7c6ece70dc068c5 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -3,8 +3,7 @@ class Groups::GroupMembersController < Groups::ApplicationController # Authorize before_action :authorize_read_group! - before_action :authorize_admin_group!, except: [:index, :leave] - before_action :authorize_admin_group_member!, only: [:create, :resend_invite] + before_action :authorize_admin_group_member!, except: [:index, :leave] def index @project = @group.projects.find(params[:project_id]) if params[:project_id] @@ -17,7 +16,8 @@ class Groups::GroupMembersController < Groups::ApplicationController end @members = @members.order('access_level DESC').page(params[:page]).per(50) - @group_member = GroupMember.new + + @group_member = @group.group_members.new end def create @@ -27,24 +27,23 @@ class Groups::GroupMembersController < Groups::ApplicationController end def update - @member = @group.group_members.find(params[:id]) + @group_member = @group.group_members.find(params[:id]) - return render_403 unless can?(current_user, :update_group_member, @member) + return render_403 unless can?(current_user, :update_group_member, @group_member) - @member.update_attributes(member_params) + @group_member.update_attributes(member_params) end def destroy @group_member = @group.group_members.find(params[:id]) - if can?(current_user, :destroy_group_member, @group_member) # May fail if last owner. - @group_member.destroy - respond_to do |format| - format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } - format.js { render nothing: true } - end - else - return render_403 + return render_403 unless can?(current_user, :destroy_group_member, @group_member) + + @group_member.destroy + + respond_to do |format| + format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' } + format.js { render nothing: true } end end @@ -63,10 +62,11 @@ class Groups::GroupMembersController < Groups::ApplicationController end def leave - @group_member = @group.group_members.where(user_id: current_user.id).first + @group_member = @group.group_members.find_by(user_id: current_user) if can?(current_user, :destroy_group_member, @group_member) @group_member.destroy + redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.") else if @group.last_owner?(current_user) diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 10233222ee1b8f76c6a78fe1a29c05cfbf33122a..0c2a350bc39ef24b1ab809d4ee5d75d4d6c4cfc1 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -46,7 +46,7 @@ class Groups::MilestonesController < Groups::ApplicationController end def milestone_path(title) - group_milestone_path(@group, title.parameterize, title: title) + group_milestone_path(@group, title.to_slug.to_s, title: title) end def projects diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 2025158d06598cc86e42af1a232fb4ad2ff823cd..f74daff3bd03f487ee0f446f499ba7186ce79291 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -40,7 +40,9 @@ class PasswordsController < Devise::PasswordsController def throttle_reset return unless resource && resource.recently_sent_password_reset? - redirect_to new_password_path(resource_name), - alert: I18n.t('devise.passwords.recently_reset') + # Throttle reset attempts, but return a normal message to + # avoid user enumeration attack. + redirect_to new_user_session_path, + notice: I18n.t('devise.passwords.send_paranoid_instructions') end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 93738aa1ee56eff11db08449cae911d948e20880..31a33bfd237ca896332cd538f8424126f6b4d89a 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -1,6 +1,7 @@ # Controller for viewing a file's blame class Projects::BlobController < Projects::ApplicationController include ExtractsPath + include CreatesMergeRequestForCommit include ActionView::Helpers::SanitizeHelper # Raised when given an invalid file path @@ -22,21 +23,9 @@ class Projects::BlobController < Projects::ApplicationController end def create - result = Files::CreateService.new(@project, current_user, @commit_params).execute - - if result[:status] == :success - flash[:notice] = "The changes have been successfully committed" - respond_to do |format| - format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) } - format.json { render json: { message: "success", filePath: namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) } } - end - else - flash[:alert] = result[:message] - respond_to do |format| - format.html { render :new } - format.json { render json: { message: "failed", filePath: namespace_project_blob_path(@project.namespace, @project, @id) } } - end - end + create_commit(Files::CreateService, success_path: after_create_path, + failure_view: :new, + failure_path: namespace_project_new_blob_path(@project.namespace, @project, @ref)) end def show @@ -47,21 +36,9 @@ class Projects::BlobController < Projects::ApplicationController end def update - result = Files::UpdateService.new(@project, current_user, @commit_params).execute - - if result[:status] == :success - flash[:notice] = "Your changes have been successfully committed" - respond_to do |format| - format.html { redirect_to after_edit_path } - format.json { render json: { message: "success", filePath: after_edit_path } } - end - else - flash[:alert] = result[:message] - respond_to do |format| - format.html { render :edit } - format.json { render json: { message: "failed", filePath: namespace_project_new_blob_path(@project.namespace, @project, @id) } } - end - end + create_commit(Files::UpdateService, success_path: after_edit_path, + failure_view: :edit, + failure_path: namespace_project_blob_path(@project.namespace, @project, @id)) end def preview @@ -77,7 +54,7 @@ class Projects::BlobController < Projects::ApplicationController if result[:status] == :success flash[:notice] = "Your changes have been successfully committed" - redirect_to namespace_project_tree_path(@project.namespace, @project, @target_branch) + redirect_to after_destroy_path else flash[:alert] = result[:message] render :show @@ -131,15 +108,51 @@ class Projects::BlobController < Projects::ApplicationController render_404 end + def create_commit(service, success_path:, failure_view:, failure_path:) + result = service.new(@project, current_user, @commit_params).execute + + if result[:status] == :success + flash[:notice] = "Your changes have been successfully committed" + respond_to do |format| + format.html { redirect_to success_path } + format.json { render json: { message: "success", filePath: success_path } } + end + else + flash[:alert] = result[:message] + respond_to do |format| + format.html { render failure_view } + format.json { render json: { message: "failed", filePath: failure_path } } + end + end + end + + def after_create_path + @after_create_path ||= + if create_merge_request? + new_merge_request_path + else + namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @file_path)) + end + end + def after_edit_path @after_edit_path ||= - if from_merge_request + if create_merge_request? + new_merge_request_path + elsif from_merge_request && @new_branch == @ref diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) + "#file-path-#{hexdigest(@path)}" - elsif @target_branch.present? - namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path)) else - namespace_project_blob_path(@project.namespace, @project, @id) + namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @path)) + end + end + + def after_destroy_path + @after_destroy_path ||= + if create_merge_request? + new_merge_request_path + else + namespace_project_tree_path(@project.namespace, @project, @new_branch) end end @@ -154,7 +167,7 @@ class Projects::BlobController < Projects::ApplicationController def editor_variables @current_branch = @ref - @target_branch = params[:new_branch].present? ? sanitized_new_branch_name : @ref + @new_branch = params[:new_branch].present? ? sanitized_new_branch_name : @ref @file_path = if action_name.to_s == 'create' @@ -174,7 +187,7 @@ class Projects::BlobController < Projects::ApplicationController @commit_params = { file_path: @file_path, current_branch: @current_branch, - target_branch: @target_branch, + target_branch: @new_branch, commit_message: params[:commit_message], file_content: params[:content], file_content_encoding: params[:encoding] diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 4638f77b887e7440ea0b63a3e7629ff62b919d67..3551f7fbd97a8b0ee5bbe9f8571a5499a37c9b26 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -77,7 +77,7 @@ class Projects::BuildsController < Projects::ApplicationController private def build - @build ||= ci_project.builds.unscoped.find_by!(id: params[:id]) + @build ||= ci_project.builds.find_by!(id: params[:id]) end def artifacts_file diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 55134e11d15907c673f04219f34049e694f472d4..5200d609cc98c458f19c438fa7c7630456e59573 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -20,8 +20,8 @@ class Projects::CompareController < Projects::ApplicationController if compare_result @commits = Commit.decorate(compare_result.commits, @project) @diffs = compare_result.diffs - @commit = @commits.last - @first_commit = @commits.first + @commit = @project.commit(head_ref) + @first_commit = @project.commit(base_ref) @line_notes = [] end end diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index 066b66014f8725ce6c7dd50db8d472d4208e1957..fb8788f0818bbbd7d4b022b1dab647ec45035460 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -28,8 +28,8 @@ class Projects::ImportsController < Projects::ApplicationController if @project.import_finished? redirect_to(project_path(@project)) and return else - redirect_to new_namespace_project_import_path(@project.namespace, - @project) && return + redirect_to(new_namespace_project_import_path(@project.namespace, + @project)) and return end end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index e74c2905e4849dd3230c6729508cb5719435550b..5250a0f5e6701efc287177814defc1aadeccc383 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -60,7 +60,7 @@ class Projects::IssuesController < Projects::ApplicationController def show @participants = @issue.participants(current_user) @note = @project.notes.new(noteable: @issue) - @notes = @issue.notes.with_associations.fresh + @notes = @issue.notes.nonawards.with_associations.fresh @noteable = @issue respond_with(@issue) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 188f0cc4cea632cfce7c96570c80475a0d8a356e..6378a1f56b0a631ba045f622ef230774b5bc97b6 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -254,7 +254,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController # Build a note object for comment form @note = @project.notes.new(noteable: @merge_request) - @notes = @merge_request.mr_and_commit_notes.inc_author.fresh + @notes = @merge_request.mr_and_commit_notes.nonawards.inc_author.fresh @discussions = Note.discussions_from_notes(@notes) @noteable = @merge_request diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 41cd08c93c60bfed5fa7ac6f0e14c85055f86162..ae6e9f6fd3857dd51a88b02f37fa46abb3955c85 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -3,7 +3,7 @@ class Projects::NotesController < Projects::ApplicationController before_action :authorize_read_note! before_action :authorize_create_note!, only: [:create] before_action :authorize_admin_note!, only: [:update, :destroy] - before_action :find_current_user_notes, except: [:destroy, :delete_attachment] + before_action :find_current_user_notes, except: [:destroy, :delete_attachment, :award_toggle] def index current_fetched_at = Time.now.to_i @@ -13,7 +13,8 @@ class Projects::NotesController < Projects::ApplicationController @notes.each do |note| notes_json[:notes] << { id: note.id, - html: note_to_html(note) + html: note_to_html(note), + valid: note.valid? } end @@ -58,6 +59,30 @@ class Projects::NotesController < Projects::ApplicationController end end + def award_toggle + noteable = if note_params[:noteable_type] == "issue" + project.issues.find(note_params[:noteable_id]) + else + project.merge_requests.find(note_params[:noteable_id]) + end + + data = { + author: current_user, + is_award: true, + note: note_params[:note].gsub(":", '') + } + + note = noteable.notes.find_by(data) + + if note + note.destroy + else + Notes::CreateService.new(project, current_user, note_params).execute + end + + render json: { ok: true } + end + private def note @@ -107,13 +132,25 @@ class Projects::NotesController < Projects::ApplicationController end def render_note_json(note) - render json: { - id: note.id, - discussion_id: note.discussion_id, - html: note_to_html(note), - discussion_html: note_to_discussion_html(note), - discussion_with_diff_html: note_to_discussion_with_diff_html(note) - } + if note.valid? + render json: { + valid: true, + id: note.id, + discussion_id: note.discussion_id, + html: note_to_html(note), + award: note.is_award, + emoji_path: note.is_award ? view_context.image_url(::AwardEmoji.path_to_emoji_image(note.note)) : "", + note: note.note, + discussion_html: note_to_discussion_html(note), + discussion_with_diff_html: note_to_discussion_with_diff_html(note) + } + else + render json: { + valid: false, + award: note.is_award, + errors: note.errors + } + end end def authorize_admin_note! diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 9de5269cd2520abaea1b308e694dfd3999754f9d..07eb94e4f48dd9967b7ac435f73702f56e661c30 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -1,6 +1,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController # Authorize - before_action :authorize_admin_project!, except: :leave + before_action :authorize_admin_project_member!, except: :leave def index @project_members = @project.project_members @@ -29,10 +29,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController @project_member = @project.project_members.new end - def new - @project_member = @project.project_members.new - end - def create @project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user) @@ -41,11 +37,17 @@ class Projects::ProjectMembersController < Projects::ApplicationController def update @project_member = @project.project_members.find(params[:id]) + + return render_403 unless can?(current_user, :update_project_member, @project_member) + @project_member.update_attributes(member_params) end def destroy @project_member = @project.project_members.find(params[:id]) + + return render_403 unless can?(current_user, :destroy_project_member, @project_member) + @project_member.destroy respond_to do |format| @@ -71,16 +73,22 @@ class Projects::ProjectMembersController < Projects::ApplicationController end def leave - if @project.namespace == current_user.namespace - message = 'You can not leave your own project. Transfer or delete the project.' - return redirect_back_or_default(default: { action: 'index' }, options: { alert: message }) - end + @project_member = @project.project_members.find_by(user_id: current_user) - @project.project_members.find_by(user_id: current_user).destroy + if can?(current_user, :destroy_project_member, @project_member) + @project_member.destroy - respond_to do |format| - format.html { redirect_to dashboard_projects_path } - format.js { render nothing: true } + respond_to do |format| + format.html { redirect_to dashboard_projects_path, notice: "You left the project." } + format.js { render nothing: true } + end + else + if current_user == @project.owner + message = 'You can not leave your own project. Transfer or delete the project.' + redirect_back_or_default(default: { action: 'index' }, options: { alert: message }) + else + render_403 + end end end diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index bdcb1a3e297ee178eb58f15157f06f61db037d81..8f272ad1281fef7306c646248ff67acf1f18465d 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -1,6 +1,7 @@ # Controller for viewing a repository's file structure class Projects::TreeController < Projects::ApplicationController include ExtractsPath + include CreatesMergeRequestForCommit include ActionView::Helpers::SanitizeHelper before_action :require_non_empty_project, except: [:new, :create] @@ -43,7 +44,7 @@ class Projects::TreeController < Projects::ApplicationController if result && result[:status] == :success flash[:notice] = "The directory has been successfully created" respond_to do |format| - format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @dir_name)) } + format.html { redirect_to after_create_dir_path } end else flash[:alert] = message @@ -53,6 +54,8 @@ class Projects::TreeController < Projects::ApplicationController end end + private + def assign_dir_vars @new_branch = params[:new_branch].present? ? sanitize(strip_tags(params[:new_branch])) : @ref @dir_name = File.join(@path, params[:dir_name]) @@ -63,4 +66,12 @@ class Projects::TreeController < Projects::ApplicationController commit_message: params[:commit_message], } end + + def after_create_dir_path + if create_merge_request? + new_merge_request_path + else + namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @dir_name)) + end + end end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 88fccfed5099e837b026783e8ff1ee25f5fb74c0..712e73bb96cfae7163b809b0b1860dfc5986589d 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -16,6 +16,9 @@ class Projects::WikisController < Projects::ApplicationController if @page render 'show' elsif file = @project_wiki.find_file(params[:id], params[:version_id]) + response.headers['Content-Security-Policy'] = "default-src 'none'" + response.headers['X-Content-Security-Policy'] = "default-src 'none'" + if file.on_disk? send_file file.on_disk_path, disposition: 'inline' else diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 23453195e8521a3603beca537b2503432a6b8c19..10c75370d7bdb66f16d85ae27b07f608836d938d 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -123,7 +123,7 @@ class ProjectsController < ApplicationController ::Projects::DestroyService.new(@project, current_user, {}).execute flash[:alert] = "Project '#{@project.name}' was deleted." - redirect_back_or_default(default: dashboard_projects_path, options: {}) + redirect_to dashboard_projects_path rescue Projects::DestroyService::DestroyError => ex redirect_to edit_project_path(@project), alert: ex.message end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 9f9f9a92f1150439032f213787824428e8f07121..c72df73af46856e2f23184a602034ff403a611da 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,6 +1,9 @@ class SnippetsController < ApplicationController before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] + # Allow read snippet + before_action :authorize_read_snippet!, only: [:show, :raw] + # Allow modify snippet before_action :authorize_update_snippet!, only: [:edit, :update] @@ -79,10 +82,14 @@ class SnippetsController < ApplicationController [Snippet::PUBLIC, Snippet::INTERNAL]). find(params[:id]) else - PersonalSnippet.are_public.find(params[:id]) + PersonalSnippet.find(params[:id]) end end + def authorize_read_snippet! + authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet) + end + def authorize_update_snippet! return render_404 unless can?(current_user, :update_personal_snippet, @snippet) end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 1484356a7f49ec79472bc61ca17104afc4796a58..30cb869eb2a589cb9db99a988752f45495727b8b 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -3,14 +3,11 @@ class UsersController < ApplicationController before_action :set_user def show - @contributed_projects = contributed_projects.joined(@user). - reject(&:forked?) + @contributed_projects = contributed_projects.joined(@user).reject(&:forked?) - @projects = @user.personal_projects. - where(id: authorized_projects_ids).includes(:namespace) + @projects = PersonalProjectsFinder.new(@user).execute(current_user) - # Collect only groups common for both users - @groups = @user.groups & GroupsFinder.new.execute(current_user) + @groups = JoinedGroupsFinder.new(@user).execute(current_user) respond_to do |format| format.html @@ -53,16 +50,8 @@ class UsersController < ApplicationController @user = User.find_by_username!(params[:username]) end - def authorized_projects_ids - # Projects user can view - @authorized_projects_ids ||= - ProjectsFinder.new.execute(current_user).pluck(:id) - end - def contributed_projects - @contributed_projects = Project. - where(id: authorized_projects_ids & @user.contributed_projects_ids). - includes(:namespace) + ContributedProjectsFinder.new(@user).execute(current_user) end def contributions_calendar @@ -73,9 +62,13 @@ class UsersController < ApplicationController def load_events # Get user activity feed for projects common for both users @events = @user.recent_events. - where(project_id: authorized_projects_ids). - with_associations + merge(projects_for_current_user). + references(:project). + with_associations. + limit_recent(20, params[:offset]) + end - @events = @events.limit(20).offset(params[:offset] || 0) + def projects_for_current_user + ProjectsFinder.new.execute(current_user) end end diff --git a/app/finders/contributed_projects_finder.rb b/app/finders/contributed_projects_finder.rb new file mode 100644 index 0000000000000000000000000000000000000000..0209649b017f672b4b5b65c68e637419dc1b379c --- /dev/null +++ b/app/finders/contributed_projects_finder.rb @@ -0,0 +1,37 @@ +class ContributedProjectsFinder + def initialize(user) + @user = user + end + + # Finds the projects "@user" contributed to, limited to either public projects + # or projects visible to the given user. + # + # current_user - When given the list of the projects is limited to those only + # visible by this user. + # + # Returns an ActiveRecord::Relation. + def execute(current_user = nil) + if current_user + relation = projects_visible_to_user(current_user) + else + relation = public_projects + end + + relation.includes(:namespace).order_id_desc + end + + private + + def projects_visible_to_user(current_user) + authorized = @user.contributed_projects.visible_to_user(current_user) + + union = Gitlab::SQL::Union. + new([authorized.select(:id), public_projects.select(:id)]) + + Project.where("projects.id IN (#{union.to_sql})") + end + + def public_projects + @user.contributed_projects.public_only + end +end diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb index b5f3176461c5a2894ae0f3ceb9e2b1512d667d05..91cb0f228f0a85abab9817c1c7dc8016568230bd 100644 --- a/app/finders/groups_finder.rb +++ b/app/finders/groups_finder.rb @@ -1,39 +1,44 @@ class GroupsFinder - def execute(current_user, options = {}) - all_groups(current_user) + # Finds the groups available to the given user. + # + # current_user - The user to find the groups for. + # + # Returns an ActiveRecord::Relation. + def execute(current_user = nil) + if current_user + relation = groups_visible_to_user(current_user) + else + relation = public_groups + end + + relation.order_id_desc end private - def all_groups(current_user) - group_ids = if current_user - if current_user.authorized_groups.any? - # User has access to groups - # - # Return only: - # groups with public projects - # groups with internal projects - # groups with joined projects - # - Project.public_and_internal_only.pluck(:namespace_id) + - current_user.authorized_groups.pluck(:id) - else - # User has no group membership - # - # Return only: - # groups with public projects - # groups with internal projects - # - Project.public_and_internal_only.pluck(:namespace_id) - end - else - # Not authenticated - # - # Return only: - # groups with public projects - Project.public_only.pluck(:namespace_id) - end - - Group.where("public IS TRUE OR id IN(?)", group_ids) + # This method returns the groups "current_user" can see. + def groups_visible_to_user(current_user) + base = groups_for_projects(public_and_internal_projects) + + union = Gitlab::SQL::Union. + new([base.select(:id), current_user.authorized_groups.select(:id)]) + + Group.where("namespaces.id IN (#{union.to_sql})") + end + + def public_groups + groups_for_projects(public_projects) + end + + def groups_for_projects(projects) + Group.public_and_given_groups(projects.select(:namespace_id)) + end + + def public_projects + Project.unscoped.public_only + end + + def public_and_internal_projects + Project.unscoped.public_and_internal_only end end diff --git a/app/finders/joined_groups_finder.rb b/app/finders/joined_groups_finder.rb new file mode 100644 index 0000000000000000000000000000000000000000..e7523136fea6a8850480bb2457d8157151849858 --- /dev/null +++ b/app/finders/joined_groups_finder.rb @@ -0,0 +1,49 @@ +# Class for finding the groups a user is a member of. +class JoinedGroupsFinder + def initialize(user = nil) + @user = user + end + + # Finds the groups of the source user, optionally limited to those visible to + # the current user. + # + # current_user - If given the groups of "@user" will only include the groups + # "current_user" can also see. + # + # Returns an ActiveRecord::Relation. + def execute(current_user = nil) + if current_user + relation = groups_visible_to_user(current_user) + else + relation = public_groups + end + + relation.order_id_desc + end + + private + + # Returns the groups the user in "current_user" can see. + # + # This list includes all public/internal projects as well as the projects of + # "@user" that "current_user" also has access to. + def groups_visible_to_user(current_user) + base = @user.authorized_groups.visible_to_user(current_user) + extra = public_and_internal_groups + union = Gitlab::SQL::Union.new([base.select(:id), extra.select(:id)]) + + Group.where("namespaces.id IN (#{union.to_sql})") + end + + def public_groups + groups_for_projects(@user.authorized_projects.public_only) + end + + def public_and_internal_groups + groups_for_projects(@user.authorized_projects.public_and_internal_only) + end + + def groups_for_projects(projects) + @user.groups.public_and_given_groups(projects.select(:namespace_id)) + end +end diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index ab252821b52133c5d2294d4f8ee4e530b3bcfafa..fa4c635f55cfc589b78d01a38a120e0dbfae4cfc 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -12,9 +12,9 @@ class NotesFinder when "commit" project.notes.for_commit_id(target_id).not_inline when "issue" - project.issues.find(target_id).notes.inc_author + project.issues.find(target_id).notes.nonawards.inc_author when "merge_request" - project.merge_requests.find(target_id).mr_and_commit_notes.inc_author + project.merge_requests.find(target_id).mr_and_commit_notes.nonawards.inc_author when "snippet", "project_snippet" project.snippets.find(target_id).notes else diff --git a/app/finders/personal_projects_finder.rb b/app/finders/personal_projects_finder.rb new file mode 100644 index 0000000000000000000000000000000000000000..a61ffa229900574be7d1e0c8304b4c29baa7d780 --- /dev/null +++ b/app/finders/personal_projects_finder.rb @@ -0,0 +1,41 @@ +class PersonalProjectsFinder + def initialize(user) + @user = user + end + + # Finds the projects belonging to the user in "@user", limited to either + # public projects or projects visible to the given user. + # + # current_user - When given the list of projects is limited to those only + # visible by this user. + # + # Returns an ActiveRecord::Relation. + def execute(current_user = nil) + if current_user + relation = projects_visible_to_user(current_user) + else + relation = public_projects + end + + relation.includes(:namespace).order_id_desc + end + + private + + def projects_visible_to_user(current_user) + authorized = @user.personal_projects.visible_to_user(current_user) + + union = Gitlab::SQL::Union. + new([authorized.select(:id), public_and_internal_projects.select(:id)]) + + Project.where("projects.id IN (#{union.to_sql})") + end + + def public_projects + @user.personal_projects.public_only + end + + def public_and_internal_projects + @user.personal_projects.public_and_internal_only + end +end diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index c81bb51583af82d3a012f21a025d31bd2444e933..dd35c215c501afdf739d82324e6e80570e36725f 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -1,11 +1,39 @@ class ProjectsFinder - def execute(current_user, options = {}) + # Returns all projects, optionally including group projects a user has access + # to. + # + # ## Examples + # + # Retrieving all public projects: + # + # ProjectsFinder.new.execute + # + # Retrieving all public/internal projects and those the given user has access + # to: + # + # ProjectsFinder.new.execute(some_user) + # + # Retrieving all public/internal projects as well as the group's projects the + # user has access to: + # + # ProjectsFinder.new.execute(some_user, group: some_group) + # + # Returns an ActiveRecord::Relation. + def execute(current_user = nil, options = {}) group = options[:group] if group - group_projects(current_user, group) + base, extra = group_projects(current_user, group) else - all_projects(current_user) + base, extra = all_projects(current_user) + end + + if base and extra + union = Gitlab::SQL::Union.new([base.select(:id), extra.select(:id)]) + + Project.where("projects.id IN (#{union.to_sql})") + else + base end end @@ -13,77 +41,36 @@ class ProjectsFinder def group_projects(current_user, group) if current_user - if group.users.include?(current_user) - # User is group member - # - # Return ALL group projects - group.projects - else - projects_members = ProjectMember.in_projects(group.projects). - with_user(current_user) - - if projects_members.any? - # User is a project member - # - # Return only: - # public projects - # internal projects - # joined projects - # - group.projects.where( - "projects.id IN (?) OR projects.visibility_level IN (?)", - projects_members.pluck(:source_id), - Project.public_and_internal_levels - ) - else - # User has no access to group or group projects - # - # Return only: - # public projects - # internal projects - # - group.projects.public_and_internal_only - end - end + [ + group_projects_for_user(current_user, group), + group.projects.public_and_internal_only + ] else - # Not authenticated - # - # Return only: - # public projects - group.projects.public_only + [group.projects.public_only] end end def all_projects(current_user) if current_user - if current_user.authorized_projects.any? - # User has access to private projects - # - # Return only: - # public projects - # internal projects - # joined projects - # - Project.where( - "projects.id IN (?) OR projects.visibility_level IN (?)", - current_user.authorized_projects.pluck(:id), - Project.public_and_internal_levels - ) - else - # User has no access to private projects - # - # Return only: - # public projects - # internal projects - # - Project.public_and_internal_only - end + [current_user.authorized_projects, public_and_internal_projects] else - # Not authenticated - # - # Return only: - # public projects - Project.public_only + [Project.public_only] end end + + def group_projects_for_user(current_user, group) + if group.users.include?(current_user) + group.projects + else + group.projects.visible_to_user(current_user) + end + end + + def public_projects + Project.unscoped.public_only + end + + def public_and_internal_projects + Project.unscoped.public_and_internal_only + end end diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index 07b5759443b2cf8346cc34f3858b3196bfa87d8b..c4d8e967a3d3bb48c6d63bfbca33c10b888fed12 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -51,7 +51,7 @@ class SnippetsFinder snippets = project.snippets.fresh.non_expired 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/diff_helper.rb b/app/helpers/diff_helper.rb index b889fb2897349f20ec7b80cce26843495f08d252..0fd5c98658808e29e26d447a02d1d55447d7e0dd 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -1,4 +1,8 @@ module DiffHelper + def diff_view + params[:view] == 'parallel' ? 'parallel' : 'inline' + end + def allowed_diff_size if diff_hard_limit_enabled? Commit::DIFF_HARD_LIMIT_FILES @@ -137,7 +141,7 @@ module DiffHelper # Always use HTML to handle case where JSON diff rendered this button params_copy.delete(:format) - link_to url_for(params_copy), id: "inline-diff-btn", class: (params[:view] != 'parallel' ? 'btn active' : 'btn') do + link_to url_for(params_copy), id: "inline-diff-btn", class: (diff_view == 'inline' ? 'btn active' : 'btn') do 'Inline' end end @@ -148,7 +152,7 @@ module DiffHelper # Always use HTML to handle case where JSON diff rendered this button params_copy.delete(:format) - link_to url_for(params_copy), id: "parallel-diff-btn", class: (params[:view] == 'parallel' ? 'btn active' : 'btn') do + link_to url_for(params_copy), id: "parallel-diff-btn", class: (diff_view == 'parallel' ? 'btn active' : 'btn') do 'Side-by-side' end end @@ -171,7 +175,7 @@ module DiffHelper def commit_for_diff(diff) if diff.deleted_file first_commit = @first_commit || @commit - first_commit.parent + first_commit.parent || @first_commit else @commit end diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 45788ba95ac889eca40e85d23ff15549136a1ae8..41b5bd7be9060b5c7b658a92680a7e9bc9e2ffc8 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -28,6 +28,8 @@ module EmailsHelper return "View #{action.humanize.singularize}" end end + + nil end def color_email_diff(diffcontent) diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index beb083d82dc7640ec7d28175b1bd8f650d45ad95..493f370d9a9bc1b33c76e57e5fd9e3e1f199d140 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -87,6 +87,33 @@ module IssuesHelper merge_requests.map(&:to_reference).to_sentence(last_word_connector: ', or ') end + def url_to_emoji(name) + emoji_path = ::AwardEmoji.path_to_emoji_image(name) + url_to_image(emoji_path) + rescue StandardError + "" + end + + def emoji_author_list(notes, current_user) + list = notes.map do |note| + note.author == current_user ? "me" : note.author.username + end + + list.join(", ") + end + + def emoji_list + ::AwardEmoji::EMOJI_LIST + end + + def note_active_class(notes, current_user) + if current_user && notes.pluck(:author_id).include?(current_user.id) + "active" + else + "" + end + end + # Required for Gitlab::Markdown::IssueReferenceFilter module_function :url_for_issue end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 728d877ace226e73b574c191fec62875a3f22415..b804d4f4e3b684c6e6bb901b944899f01fbdb20e 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -8,14 +8,6 @@ module MergeRequestsHelper ) end - def new_mr_path_for_fork_from_push_event(event) - new_namespace_project_merge_request_path( - event.project.namespace, - event.project, - new_mr_from_push_event(event, event.project.forked_from_project) - ) - end - def new_mr_from_push_event(event, target_project) { merge_request: { diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 690ae2090db69c55e4223d1664b1bfbafbcb4544..c9cd4a0d54cbeca002277f7912ac73b41a6d484e 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -253,14 +253,6 @@ module ProjectsHelper filename_path(project, :version) end - def hidden_pass_url(original_url) - result = URI(original_url) - result.password = '*****' unless result.password.nil? - result - rescue - original_url - end - def project_wiki_path_with_version(proj, page, version, is_newest) url_params = is_newest ? {} : { version_id: version } namespace_project_wiki_path(proj.namespace, proj, page, url_params) diff --git a/app/models/ability.rb b/app/models/ability.rb index d01b3ae6f050a88b91e996212cab05546811c50d..07f3a56ec7abd8c8d245d33c398e2c32475efc54 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -1,8 +1,8 @@ class Ability class << self def allowed(user, subject) - return not_auth_abilities(user, subject) if user.nil? - return [] unless user.kind_of?(User) + return anonymous_abilities(user, subject) if user.nil? + return [] unless user.is_a?(User) return [] if user.blocked? case subject.class.name @@ -15,19 +15,30 @@ class Ability when "Group" then group_abilities(user, subject) when "Namespace" then namespace_abilities(user, subject) when "GroupMember" then group_member_abilities(user, subject) + when "ProjectMember" then project_member_abilities(user, subject) else [] end.concat(global_abilities(user)) end - # List of possible abilities - # for non-authenticated user - def not_auth_abilities(user, subject) - project = if subject.kind_of?(Project) + # List of possible abilities for anonymous user + def anonymous_abilities(user, subject) + case true + when subject.is_a?(PersonalSnippet) + anonymous_personal_snippet_abilities(subject) + when subject.is_a?(Project) || subject.respond_to?(:project) + anonymous_project_abilities(subject) + when subject.is_a?(Group) || subject.respond_to?(:group) + anonymous_group_abilities(subject) + else + [] + end + end + + def anonymous_project_abilities(subject) + project = if subject.is_a?(Project) subject - elsif subject.respond_to?(:project) - subject.project else - nil + subject.project end if project && project.public? @@ -47,19 +58,29 @@ class Ability rules - project_disabled_features_rules(project) else - group = if subject.kind_of?(Group) - subject - elsif subject.respond_to?(:group) - subject.group - else - nil - end + [] + end + end - if group && group.public_profile? - [:read_group] - else - [] - end + def anonymous_group_abilities(subject) + group = if subject.is_a?(Group) + subject + else + subject.group + end + + if group && group.public_profile? + [:read_group] + else + [] + end + end + + def anonymous_personal_snippet_abilities(snippet) + if snippet.public? + [:read_personal_snippet] + else + [] end end @@ -231,19 +252,19 @@ class Ability # Only group masters and group owners can create new projects in group if group.has_master?(user) || group.has_owner?(user) || user.admin? - rules.push(*[ + rules += [ :create_projects, :admin_milestones - ]) + ] end # Only group owner and administrators can admin group if group.has_owner?(user) || user.admin? - rules.push(*[ + rules += [ :admin_group, :admin_namespace, :admin_group_member - ]) + ] end rules.flatten @@ -254,16 +275,15 @@ class Ability # Only namespace owner and administrators can admin it if namespace.owner == user || user.admin? - rules.push(*[ + rules += [ :create_projects, :admin_namespace - ]) + ] end rules.flatten end - [:issue, :merge_request].each do |name| define_method "#{name}_abilities" do |user, subject| rules = [] @@ -280,7 +300,7 @@ class Ability end end - [:note, :project_snippet, :personal_snippet].each do |name| + [:note, :project_snippet].each do |name| define_method "#{name}_abilities" do |user, subject| rules = [] @@ -300,19 +320,61 @@ class Ability end end + def personal_snippet_abilities(user, snippet) + rules = [] + + if snippet.author == user + rules += [ + :read_personal_snippet, + :update_personal_snippet, + :admin_personal_snippet + ] + end + + if snippet.public? || snippet.internal? + rules << :read_personal_snippet + end + + rules + end + def group_member_abilities(user, subject) rules = [] target_user = subject.user group = subject.group - can_manage = group_abilities(user, group).include?(:admin_group_member) - if can_manage && (user != target_user) - rules << :update_group_member - rules << :destroy_group_member + unless group.last_owner?(target_user) + can_manage = group_abilities(user, group).include?(:admin_group_member) + + if can_manage && user != target_user + rules << :update_group_member + rules << :destroy_group_member + end + + if user == target_user + rules << :destroy_group_member + end end - if !group.last_owner?(user) && (can_manage || (user == target_user)) - rules << :destroy_group_member + rules + end + + def project_member_abilities(user, subject) + rules = [] + target_user = subject.user + project = subject.project + + unless target_user == project.owner + can_manage = project_abilities(user, project).include?(:admin_project_member) + + if can_manage && user != target_user + rules << :update_project_member + rules << :destroy_project_member + end + + if user == target_user + rules << :destroy_project_member + end end rules @@ -320,10 +382,10 @@ class Ability def abilities @abilities ||= begin - abilities = Six.new - abilities << self - abilities - end + abilities = Six.new + abilities << self + abilities + end end private diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 9e70247ef517dfaf5f944e0aad0d2ba79917ddeb..5ddcf3d9a0b19895859bc249bcc9756673be7d19 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -30,6 +30,8 @@ # class ApplicationSetting < ActiveRecord::Base + CACHE_KEY = 'application_setting.last' + serialize :restricted_visibility_levels serialize :import_sources serialize :restricted_signup_domains, Array @@ -73,15 +75,19 @@ class ApplicationSetting < ActiveRecord::Base end after_commit do - Rails.cache.write('application_setting.last', self) + Rails.cache.write(CACHE_KEY, self) end def self.current - Rails.cache.fetch('application_setting.last') do + Rails.cache.fetch(CACHE_KEY) do ApplicationSetting.last end end + def self.expire + Rails.cache.delete(CACHE_KEY) + end + def self.create_from_defaults create( default_projects_limit: Settings.gitlab['default_projects_limit'], @@ -99,7 +105,7 @@ class ApplicationSetting < ActiveRecord::Base restricted_signup_domains: Settings.gitlab['restricted_signup_domains'], import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], - max_artifacts_size: Settings.gitlab_ci['max_artifacts_size'], + max_artifacts_size: Settings.artifacts['max_size'], ) end diff --git a/app/models/ci/application_setting.rb b/app/models/ci/application_setting.rb index 1307fa0b472def2dd758f3c15ee50c47dd43a473..7f5df8ce6c42e1a7a4e3fc6b62e703a997d430c4 100644 --- a/app/models/ci/application_setting.rb +++ b/app/models/ci/application_setting.rb @@ -12,13 +12,18 @@ module Ci class ApplicationSetting < ActiveRecord::Base extend Ci::Model + CACHE_KEY = 'ci_application_setting.last' after_commit do - Rails.cache.write('ci_application_setting.last', self) + Rails.cache.write(CACHE_KEY, self) + end + + def self.expire + Rails.cache.delete(CACHE_KEY) end def self.current - Rails.cache.fetch('ci_application_setting.last') do + Rails.cache.fetch(CACHE_KEY) do Ci::ApplicationSetting.last end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index e78b154084b014cc4c773b7f42b9a12c4c163624..52ce1b920fae151f74b51a208da58cd6bf3b08f1 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -97,6 +97,8 @@ module Ci state_machine :status, initial: :pending do after_transition any => [:success, :failed, :canceled] do |build, transition| + return unless build.gl_project + project = build.project if project.web_hooks? diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index 33b57173928a3fbe8ddcab0889249b255a522958..971e899de847b6c6a5043c4f37d3404e080dd628 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -188,13 +188,13 @@ module Ci end def config_processor + return nil unless ci_yaml_file @config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, gl_project.path_with_namespace) - rescue Ci::GitlabCiYamlProcessor::ValidationError => e + rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e save_yaml_error(e.message) nil - rescue Exception => e - logger.error e.message + "\n" + e.backtrace.join("\n") - save_yaml_error("Undefined yaml error") + rescue + save_yaml_error("Undefined error") nil end diff --git a/app/models/commit.rb b/app/models/commit.rb index 492f6be1ce39be2995c71c57ec2dfa0f2ce44c98..14883c96f5f3f1262ed69a4293c1f884e095361e 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -135,10 +135,10 @@ class Commit description.present? end - def hook_attrs + def hook_attrs(with_changed_files: false) path_with_namespace = project.path_with_namespace - { + data = { id: id, message: safe_message, timestamp: committed_date.xmlschema, @@ -148,6 +148,12 @@ class Commit email: author_email } } + + if with_changed_files + data.merge!(repo_changes) + end + + data end # Discover issues should be closed when this commit is pushed to a project's @@ -196,4 +202,22 @@ class Commit def status ci_commit.try(:status) || :not_found end + + private + + def repo_changes + changes = { added: [], modified: [], removed: [] } + + diffs.each do |diff| + if diff.deleted_file + changes[:removed] << diff.old_path + elsif diff.renamed_file || diff.new_file + changes[:added] << diff.new_path + else + changes[:modified] << diff.new_path + end + end + + changes + end end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 492a026add9ffdc2fd01420216a970adc850aa92..b54145ba1b0575c8ba7cc0b7c85e50f6691c5e43 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -89,39 +89,14 @@ module Issuable opened? || reopened? end - # - # Votes - # - - # Return the number of -1 comments (downvotes) + # Deprecated. Still exists to preserve API compatibility. def downvotes - filter_superceded_votes(notes.select(&:downvote?), notes).size + 0 end - def downvotes_in_percent - if votes_count.zero? - 0 - else - 100.0 - upvotes_in_percent - end - end - - # Return the number of +1 comments (upvotes) + # Deprecated. Still exists to preserve API compatibility. def upvotes - filter_superceded_votes(notes.select(&:upvote?), notes).size - end - - def upvotes_in_percent - if votes_count.zero? - 0 - else - 100.0 / votes_count * upvotes - end - end - - # Return the total number of votes - def votes_count - upvotes + downvotes + 0 end def subscribed?(user) @@ -183,18 +158,4 @@ module Issuable def notes_with_associations notes.includes(:author, :project) end - - private - - def filter_superceded_votes(votes, notes) - filteredvotes = [] + votes - - votes.each do |vote| - if vote.superceded?(notes) - filteredvotes.delete(vote) - end - end - - filteredvotes - end end diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index 913c747a1c3194bc5c98cd1f9558fdde019b520a..7391a77383c07db7fd9fff4ad0ee0a8de74bfc5a 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -8,8 +8,9 @@ module Sortable included do # By default all models should be ordered # by created_at field starting from newest - default_scope { order(id: :desc) } + default_scope { order_id_desc } + scope :order_id_desc, -> { reorder(id: :desc) } scope :order_created_desc, -> { reorder(created_at: :desc) } scope :order_created_asc, -> { reorder(created_at: :asc) } scope :order_updated_desc, -> { reorder(updated_at: :desc) } diff --git a/app/models/event.rb b/app/models/event.rb index bf64ac29d325759507834eef8be2c3c6d81c36dc..9afd223bce578c23df0b005474bdfe20ff956c7e 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -63,6 +63,16 @@ class Event < ActiveRecord::Base Event::PUSHED, ["MergeRequest", "Issue"], [Event::CREATED, Event::CLOSED, Event::MERGED]) end + + def latest_update_time + row = select(:updated_at, :project_id).reorder(id: :desc).take + + row ? row.updated_at : nil + end + + def limit_recent(limit = 20, offset = nil) + recent.limit(limit).offset(offset) + end end def proper? diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index 1321ccd963fb449491967fe1365c32394899e56a..85aa71662fe0af1afdb3706a2bee7b503cf0931f 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -16,7 +16,7 @@ class GlobalMilestone end def safe_title - @title.parameterize + @title.to_slug.to_s end def projects diff --git a/app/models/group.rb b/app/models/group.rb index 793a3b5ef2e4a92faa8512510d5bf90b898dff34..1b5b875a19e3ff8dbaef174445f5a26ca698a5ce 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -20,8 +20,9 @@ require 'file_size_validator' class Group < Namespace include Gitlab::ConfigHelper include Referable - + has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember' + alias_method :members, :group_members has_many :users, through: :group_members validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } @@ -48,6 +49,14 @@ class Group < Namespace def reference_pattern User.reference_pattern end + + def public_and_given_groups(ids) + where('public IS TRUE OR namespaces.id IN (?)', ids) + end + + def visible_to_user(user) + where(id: user.authorized_groups.select(:id).reorder(nil)) + end end def to_reference(_from_project = nil) @@ -110,10 +119,6 @@ class Group < Namespace has_owner?(user) && owners.size == 1 end - def members - group_members - end - def avatar_type unless self.avatar.image? self.errors.add :avatar, "only images allowed" diff --git a/app/models/member.rb b/app/models/member.rb index cae8caa23fb2770299ee935b82f48edecd22f39b..28aee2e379963224b4459c4b5251d10464528d63 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -30,13 +30,22 @@ class Member < ActiveRecord::Base validates :user, presence: true, unless: :invite? validates :source, presence: true - validates :user_id, uniqueness: { scope: [:source_type, :source_id], + validates :user_id, uniqueness: { scope: [:source_type, :source_id], message: "already exists in source", allow_nil: true } validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true - validates :invite_email, presence: { if: :invite? }, - email: { strict_mode: true, allow_nil: true }, - uniqueness: { scope: [:source_type, :source_id], allow_nil: true } + validates :invite_email, + presence: { + if: :invite? + }, + email: { + strict_mode: true, + allow_nil: true + }, + uniqueness: { + scope: [:source_type, :source_id], + allow_nil: true + } scope :invite, -> { where(user_id: nil) } scope :non_invite, -> { where("user_id IS NOT NULL") } @@ -73,7 +82,7 @@ class Member < ActiveRecord::Base def add_user(members, user_id, access_level, current_user = nil) user = user_for_id(user_id) - + # `user` can be either a User object or an email to be invited if user.is_a?(User) member = members.find_or_initialize_by(user_id: user.id) @@ -82,10 +91,21 @@ class Member < ActiveRecord::Base member.invite_email = user end - member.created_by ||= current_user - member.access_level = access_level + if can_update_member?(current_user, member) + member.created_by ||= current_user + member.access_level = access_level + + member.save + end + end + + private - member.save + def can_update_member?(current_user, member) + # There is no current user for bulk actions, in which case anything is allowed + !current_user || + current_user.can?(:update_group_member, member) || + current_user.can?(:update_project_member, member) end end @@ -95,7 +115,7 @@ class Member < ActiveRecord::Base def accept_invite!(new_user) return false unless invite? - + self.invite_token = nil self.invite_accepted_at = Time.now.utc diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 2eb03b8ba5b1109c770fdbd46a3ae7986aa337b9..f418f09aad80d2683145702cef06d82f909da7c1 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -288,7 +288,7 @@ class MergeRequest < ActiveRecord::Base work_in_progress: work_in_progress? } - unless last_commit.nil? + if last_commit attrs.merge!(last_commit: last_commit.hook_attrs) end @@ -473,7 +473,7 @@ class MergeRequest < ActiveRecord::Base end def ci_commit - if last_commit + if last_commit and source_project source_project.ci_commit(last_commit.id) end end diff --git a/app/models/note.rb b/app/models/note.rb index 0b3aa30abd745187bc656439d4cddac7969a532a..239a0f77f8e5ad02e55d14a0d4c9f60e4c44654a 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -39,17 +39,24 @@ class Note < ActiveRecord::Base delegate :name, to: :project, prefix: true delegate :name, :email, to: :author, prefix: true + before_validation :set_award! + validates :note, :project, presence: true + validates :note, uniqueness: { scope: [:author, :noteable_type, :noteable_id] }, if: ->(n) { n.is_award } + validates :note, inclusion: { in: Emoji.emojis_names }, if: ->(n) { n.is_award } validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true # Attachments are deprecated and are handled by Markdown uploader validates :attachment, file_size: { maximum: :max_attachment_size } validates :noteable_id, presence: true, if: ->(n) { n.noteable_type.present? && n.noteable_type != 'Commit' } validates :commit_id, presence: true, if: ->(n) { n.noteable_type == 'Commit' } + validates :author, presence: true mount_uploader :attachment, AttachmentUploader # Scopes + scope :awards, ->{ where(is_award: true) } + scope :nonawards, ->{ where(is_award: false) } scope :for_commit_id, ->(commit_id) { where(noteable_type: "Commit", commit_id: commit_id) } scope :inline, ->{ where("line_code IS NOT NULL") } scope :not_inline, ->{ where(line_code: [nil, '']) } @@ -97,6 +104,12 @@ class Note < ActiveRecord::Base def search(query) where("LOWER(note) like :query", query: "%#{query.downcase}%") end + + def grouped_awards + awards.select(:note).distinct.map do |note| + [ note.note, where(note: note.note) ] + end + end end def cross_reference? @@ -288,44 +301,6 @@ class Note < ActiveRecord::Base nil end - DOWNVOTES = %w(-1 :-1: :thumbsdown: :thumbs_down_sign:) - - # Check if the note is a downvote - def downvote? - votable? && note.start_with?(*DOWNVOTES) - end - - UPVOTES = %w(+1 :+1: :thumbsup: :thumbs_up_sign:) - - # Check if the note is an upvote - def upvote? - votable? && note.start_with?(*UPVOTES) - end - - def superceded?(notes) - return false unless vote? - - notes.each do |note| - next if note == self - - if note.vote? && - self[:author_id] == note[:author_id] && - self[:created_at] <= note[:created_at] - return true - end - end - - false - end - - def vote? - upvote? || downvote? - end - - def votable? - for_issue? || (for_merge_request? && !for_diff_line?) - end - # Mentionable override. def gfm_reference(from_project = nil) noteable.gfm_reference(from_project) @@ -363,7 +338,44 @@ class Note < ActiveRecord::Base read_attribute(:system) end + # Deprecated. Still exists to preserve API compatibility. + def downvote? + false + end + + # Deprecated. Still exists to preserve API compatibility. + def upvote? + false + end + def editable? !system? end + + # Checks if note is an award added as a comment + # + # If note is an award, this method sets is_award to true + # and changes content of the note to award name. + # + # Method is executed as a before_validation callback. + # + def set_award! + return unless awards_supported? && contains_emoji_only? + self.is_award = true + self.note = award_emoji_name + end + + private + + def awards_supported? + noteable.kind_of?(Issue) || noteable.is_a?(MergeRequest) + end + + def contains_emoji_only? + note =~ /\A#{Gitlab::Markdown::EmojiFilter.emoji_pattern}\s?\Z/ + end + + def award_emoji_name + note.match(Gitlab::Markdown::EmojiFilter.emoji_pattern)[1] + end end diff --git a/app/models/project.rb b/app/models/project.rb index 9ea0d15497acb25833a9d56c679e5ea1ce9e1f09..f0a4b6aae7b2af1d52c6a964d0569f937e52d4ec 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -42,7 +42,7 @@ class Project < ActiveRecord::Base include Sortable include AfterCommitQueue include CaseSensitivity - + extend Gitlab::ConfigHelper extend Enumerize @@ -286,6 +286,10 @@ class Project < ActiveRecord::Base joins(join_body).reorder('join_note_counts.amount DESC') end + + def visible_to_user(user) + where(id: user.authorized_projects.select(:id).reorder(nil)) + end end def team @@ -310,15 +314,17 @@ class Project < ActiveRecord::Base def add_import_job if forked? - unless RepositoryForkWorker.perform_async(id, forked_from_project.path_with_namespace, self.namespace.path) - import_fail - end + RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path) else - RepositoryImportWorker.perform_async(id) + RepositoryImportWorker.perform_async(self.id) end end def clear_import_data + update(import_error: nil) + + ProjectCacheWorker.perform_async(self.id) + self.import_data.destroy if self.import_data end @@ -346,6 +352,14 @@ class Project < ActiveRecord::Base import_status == 'finished' end + def safe_import_url + result = URI.parse(self.import_url) + result.password = '*****' unless result.password.nil? + result.to_s + rescue + original_url + end + def check_limit unless creator.can_create_project? or namespace.kind == 'group' errors[:limit_reached] << ("Your project limit is #{creator.projects_limit} projects! Please contact your administrator to increase it") diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index c240213200ddb077006042f0c9c17d18e8154daf..127684bd27457fbd1f32850a27900fcc64010269 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -32,6 +32,8 @@ class DroneCiService < CiService def compose_service_hook hook = service_hook || build_service_hook + # If using a service template, project may not be available + hook.url = [drone_url, "/api/hook", "?owner=#{project.namespace.path}", "&name=#{project.path}", "&access_token=#{token}"].join if project hook.enable_ssl_verification = enable_ssl_verification hook.save end diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb index 095d04e0df4c0a2959e796d849ab583cdfa487c8..c5657b5070edc8c2f1e018dd7a27eb77bb4e32df 100644 --- a/app/models/project_services/gitlab_ci_service.rb +++ b/app/models/project_services/gitlab_ci_service.rb @@ -30,6 +30,7 @@ class GitlabCiService < CiService end def ensure_gitlab_ci_project + return unless project project.ensure_gitlab_ci_project end diff --git a/app/models/repository.rb b/app/models/repository.rb index f76b770e867d3ab8754fcdb0b84896d8d570021b..d247b0f50124b98405277b626dee258f6bc24898 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -6,7 +6,7 @@ class Repository include Gitlab::ShellAdapter - attr_accessor :raw_repository, :path_with_namespace, :project + attr_accessor :path_with_namespace, :project def self.clean_old_archives repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path @@ -19,14 +19,18 @@ class Repository def initialize(path_with_namespace, default_branch = nil, project = nil) @path_with_namespace = path_with_namespace @project = project + end - if path_with_namespace - @raw_repository = Gitlab::Git::Repository.new(path_to_repo) - @raw_repository.autocrlf = :input - end + def raw_repository + return nil unless path_with_namespace - rescue Gitlab::Git::Repository::NoRepository - nil + @raw_repository ||= begin + repo = Gitlab::Git::Repository.new(path_to_repo) + repo.autocrlf = :input + repo + rescue Gitlab::Git::Repository::NoRepository + nil + end end # Return absolute path to repository @@ -105,29 +109,25 @@ class Repository end def add_branch(branch_name, ref) - cache.expire(:branch_names) - @branches = nil + expire_branches_cache gitlab_shell.add_branch(path_with_namespace, branch_name, ref) end def add_tag(tag_name, ref, message = nil) - cache.expire(:tag_names) - @tags = nil + expire_tags_cache gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message) end def rm_branch(branch_name) - cache.expire(:branch_names) - @branches = nil + expire_branches_cache gitlab_shell.rm_branch(path_with_namespace, branch_name) end def rm_tag(tag_name) - cache.expire(:tag_names) - @tags = nil + expire_tags_cache gitlab_shell.rm_tag(path_with_namespace, tag_name) end @@ -169,6 +169,16 @@ class Repository end end + def expire_tags_cache + cache.expire(:tag_names) + @tags = nil + end + + def expire_branches_cache + cache.expire(:branch_names) + @branches = nil + end + def expire_cache cache_keys.each do |key| cache.expire(key) @@ -484,7 +494,7 @@ class Repository root_ref_commit = commit(root_ref) if branch_commit - rugged.merge_base(root_ref_commit.id, branch_commit.id) == branch_commit.id + is_ancestor?(branch_commit.id, root_ref_commit.id) else nil end @@ -494,6 +504,11 @@ class Repository rugged.merge_base(first_commit_id, second_commit_id) end + def is_ancestor?(ancestor_id, descendant_id) + merge_base(ancestor_id, descendant_id) == ancestor_id + end + + def search_files(query, ref) offset = 2 args = %W(#{Gitlab.config.git.bin_path} grep -i -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref}) @@ -556,9 +571,13 @@ class Repository # Run GitLab pre-receive hook pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', path_to_repo) - status = pre_receive_hook.trigger(gl_id, oldrev, newrev, ref) + pre_receive_hook_status = pre_receive_hook.trigger(gl_id, oldrev, newrev, ref) + + # Run GitLab update hook + update_hook = Gitlab::Git::Hook.new('update', path_to_repo) + update_hook_status = update_hook.trigger(gl_id, oldrev, newrev, ref) - if status + if pre_receive_hook_status && update_hook_status if was_empty # Create branch rugged.references.create(ref, newrev) @@ -581,7 +600,7 @@ class Repository # Remove tmp ref and return error to user rugged.references.delete(tmp_ref) - raise PreReceiveError.new('Commit was rejected by pre-receive hook') + raise PreReceiveError.new('Commit was rejected by git hook') end end diff --git a/app/models/user.rb b/app/models/user.rb index 61abea1f6eabbc52e4dbddefc2ee96dc90774b01..9374f01f99ffc972c47cdb9a730d692410cfae57 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -389,42 +389,23 @@ class User < ActiveRecord::Base end end - # Groups user has access to + # Returns the groups a user has access to def authorized_groups - @authorized_groups ||= begin - group_ids = (groups.pluck(:id) + authorized_projects.pluck(:namespace_id)) - Group.where(id: group_ids) - end - end + union = Gitlab::SQL::Union. + new([groups.select(:id), authorized_projects.select(:namespace_id)]) - def authorized_projects_id - @authorized_projects_id ||= begin - project_ids = personal_projects.pluck(:id) - project_ids.push(*groups_projects.pluck(:id)) - project_ids.push(*projects.pluck(:id).uniq) - end - end - - def master_or_owner_projects_id - @master_or_owner_projects_id ||= begin - scope = { access_level: [ Gitlab::Access::MASTER, Gitlab::Access::OWNER ] } - project_ids = personal_projects.pluck(:id) - project_ids.push(*groups_projects.where(members: scope).pluck(:id)) - project_ids.push(*projects.where(members: scope).pluck(:id).uniq) - end + Group.where("namespaces.id IN (#{union.to_sql})") end - # Projects user has access to + # Returns the groups a user is authorized to access. def authorized_projects - @authorized_projects ||= Project.where(id: authorized_projects_id) + Project.where("projects.id IN (#{projects_union.to_sql})") end def owned_projects @owned_projects ||= - begin - namespace_ids = owned_groups.pluck(:id).push(namespace.id) - Project.in_namespace(namespace_ids).joins(:namespace) - end + Project.where('namespace_id IN (?) OR namespace_id = ?', + owned_groups.select(:id), namespace.id).joins(:namespace) end # Team membership in authorized projects @@ -739,12 +720,25 @@ class User < ActiveRecord::Base Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil) end - def contributed_projects_ids - Event.contributions.where(author_id: self). + # Returns the projects a user contributed to in the last year. + # + # This method relies on a subquery as this performs significantly better + # compared to a JOIN when coupled with, for example, + # `Project.visible_to_user`. That is, consider the following code: + # + # some_user.contributed_projects.visible_to_user(other_user) + # + # If this method were to use a JOIN the resulting query would take roughly 200 + # ms on a database with a similar size to GitLab.com's database. On the other + # hand, using a subquery means we can get the exact same data in about 40 ms. + def contributed_projects + events = Event.select(:project_id). + contributions.where(author_id: self). where("created_at > ?", Time.now - 1.year). - reorder(project_id: :desc). - select(:project_id). - uniq.map(&:project_id) + uniq. + reorder(nil) + + Project.where(id: events) end def restricted_signup_domains @@ -777,8 +771,27 @@ class User < ActiveRecord::Base def ci_authorized_runners @ci_authorized_runners ||= begin runner_ids = Ci::RunnerProject.joins(:project). - where(ci_projects: { gitlab_id: master_or_owner_projects_id }).select(:runner_id) + where("ci_projects.gitlab_id IN (#{ci_projects_union.to_sql})"). + select(:runner_id) + Ci::Runner.specific.where(id: runner_ids) end end + + private + + def projects_union + Gitlab::SQL::Union.new([personal_projects.select(:id), + groups_projects.select(:id), + projects.select(:id)]) + end + + def ci_projects_union + scope = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] } + groups = groups_projects.where(members: scope) + other = projects.where(members: scope) + + Gitlab::SQL::Union.new([personal_projects.select(:id), groups.select(:id), + other.select(:id)]) + end end diff --git a/app/services/create_release_service.rb b/app/services/create_release_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..e06a6f2f47a4d391a0bdc5ccfcf98e9fddb3f337 --- /dev/null +++ b/app/services/create_release_service.rb @@ -0,0 +1,31 @@ +require_relative 'base_service' + +class CreateReleaseService < BaseService + def execute(tag_name, release_description) + + repository = project.repository + existing_tag = repository.find_tag(tag_name) + + # Only create a release if the tag exists + if existing_tag + release = project.releases.find_by(tag: tag_name) + + if release + error('Release already exists', 409) + else + release = project.releases.new({ tag: tag_name, description: release_description }) + release.save + + success(release) + end + else + error('Tag does not exist', 404) + end + end + + def success(release) + out = super() + out[:release] = release + out + end +end diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb index 9917119fce2499cca0eaa732421d8e15bd92051d..2452999382af08dcda44a9d8f0e957e12eb7e399 100644 --- a/app/services/create_tag_service.rb +++ b/app/services/create_tag_service.rb @@ -19,16 +19,16 @@ class CreateTagService < BaseService new_tag = repository.find_tag(tag_name) if new_tag - if release_description - release = project.releases.find_or_initialize_by(tag: tag_name) - release.update_attributes(description: release_description) - end - 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) + if release_description + CreateReleaseService.new(@project, @current_user). + execute(tag_name, release_description) + end + success(new_tag) else error('Invalid reference name') diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index ccb6b97858c0f0023432b61790f608d7f9dd4cf1..f11690aa3f465e1f84167202c86a0500af74c850 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -58,12 +58,6 @@ class GitPushService @push_data = build_push_data(oldrev, newrev, ref) - # If CI was disabled but .gitlab-ci.yml file was pushed - # we enable CI automatically - if !project.builds_enabled? && gitlab_ci_yaml?(newrev) - project.enable_ci - end - EventCreateService.new.push(project, user, @push_data) project.execute_hooks(@push_data.dup, :push_hooks) project.execute_services(@push_data.dup, :push_hooks) @@ -134,10 +128,4 @@ class GitPushService def commit_user(commit) commit.author || user end - - def gitlab_ci_yaml?(sha) - @project.repository.blob_at(sha, '.gitlab-ci.yml') - rescue Rugged::ReferenceError - nil - end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 15b3825f96a3fd48b1b8dcd625edcb3c74137f4a..fcec35b03b711a0dd6b9996d6ce6f82d7afd960f 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -28,6 +28,10 @@ class IssuableBaseService < BaseService end def filter_params(issuable_ability_name = :issue) + filter_assignee + filter_milestone + filter_labels + ability = :"admin_#{issuable_ability_name}" unless can?(current_user, ability, project) @@ -36,4 +40,27 @@ class IssuableBaseService < BaseService params.delete(:assignee_id) end end + + def filter_assignee + if params[:assignee_id] == IssuableFinder::NONE + params[:assignee_id] = '' + end + end + + def filter_milestone + milestone_id = params[:milestone_id] + return unless milestone_id + + if milestone_id == IssuableFinder::NONE || + project.milestones.find_by(id: milestone_id).nil? + params[:milestone_id] = '' + end + end + + def filter_labels + return if params[:label_ids].to_a.empty? + + params[:label_ids] = + project.labels.where(id: params[:label_ids]).pluck(:id) + end end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index a9b29f9654d716553330b62c48860e4da4d7d1bd..572bddd7fd66c0535f225aa013e63237b549c80a 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -9,6 +9,9 @@ module MergeRequests merge_request.compare_commits = [] merge_request.compare_diffs = [] merge_request.source_project = project unless merge_request.source_project + + merge_request.target_project = nil unless can?(current_user, :read_project, merge_request.target_project) + merge_request.target_project ||= (project.forked_from_project || project) merge_request.target_branch ||= merge_request.target_project.default_branch diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index 2001dc89c3332fbbf91770d2dc31aefb7d244eb9..5e2eb5dc21ef4c2e234fb134327592ab8cf353ad 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -5,11 +5,13 @@ module Notes note.author = current_user note.system = false + return unless valid_project?(note) + if note.save notification_service.new_note(note) - # Skip system notes, like status changes and cross-references. - unless note.system + # Skip system notes, like status changes and cross-references and awards + unless note.system || note.is_award event_service.leave_note(note, note.author) note.create_cross_references! execute_hooks(note) @@ -28,5 +30,14 @@ module Notes note.project.execute_hooks(note_data, :note_hooks) note.project.execute_services(note_data, :note_hooks) 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/notification_service.rb b/app/services/notification_service.rb index a6b2234865047a8470101013749730185ad81454..dc78f85105c423a298e0486bcf97f97b961e9904 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -102,6 +102,7 @@ class NotificationService # ignore gitlab service messages return true if note.note.start_with?('Status changed to closed') return true if note.cross_reference? && note.system == true + return true if note.is_award target = note.noteable @@ -361,11 +362,13 @@ class NotificationService end def reassign_resource_email(target, project, current_user, method) - assignee_id_was = previous_record(target, "assignee_id") - recipients = build_recipients(target, project, current_user) + previous_assignee_id = previous_record(target, "assignee_id") + previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id + + recipients = build_recipients(target, project, current_user, [previous_assignee]) recipients.each do |recipient| - mailer.send(method, recipient.id, target.id, assignee_id_was, current_user.id) + mailer.send(method, recipient.id, target.id, previous_assignee_id, current_user.id) end end @@ -377,9 +380,11 @@ class NotificationService end end - def build_recipients(target, project, current_user) + def build_recipients(target, project, current_user, extra_recipients = nil) recipients = target.participants(current_user) + recipients = recipients.concat(extra_recipients).compact.uniq if extra_recipients + recipients = add_project_watchers(recipients, project) recipients = reject_mention_users(recipients, project) recipients = reject_muted_users(recipients, project) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 5b84527eccfcb6fb9b40475c8c6782090494552c..700a1db04d83ecf78f19fa5e1b0021eefc392be7 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -55,7 +55,9 @@ module Projects @project.save if @project.persisted? && !@project.import? - raise 'Failed to create repository' unless @project.create_repository + unless @project.create_repository + raise 'Failed to create repository' + end end end @@ -94,9 +96,7 @@ module Projects @project.team << [current_user, :master, current_user] end - if @project.import? - @project.import_start - end + @project.import_start if @project.import? end end end diff --git a/app/services/update_release_service.rb b/app/services/update_release_service.rb new file mode 100644 index 0000000000000000000000000000000000000000..25eb13ef09a4312a95ca0b7f1dd86bb1451d3f67 --- /dev/null +++ b/app/services/update_release_service.rb @@ -0,0 +1,29 @@ +require_relative 'base_service' + +class UpdateReleaseService < BaseService + def execute(tag_name, release_description) + + repository = project.repository + existing_tag = repository.find_tag(tag_name) + + if existing_tag + release = project.releases.find_by(tag: tag_name) + + if release + release.update_attributes(description: release_description) + + success(release) + else + error('Release does not exist', 404) + end + else + error('Tag does not exist', 404) + end + end + + def success(release) + out = super() + out[:release] = release + out + end +end diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb index b4e0fc5772dc101fdd760c3e1a95d6ec96cf6aca..1dccc39e7e2e4d5902948453b71e19aab275866c 100644 --- a/app/uploaders/artifact_uploader.rb +++ b/app/uploaders/artifact_uploader.rb @@ -5,15 +5,15 @@ class ArtifactUploader < CarrierWave::Uploader::Base attr_accessor :build, :field def self.artifacts_path - File.expand_path('shared/artifacts/', Rails.root) + Gitlab.config.artifacts.path end def self.artifacts_upload_path - File.expand_path('shared/artifacts/tmp/uploads/', Rails.root) + File.join(self.artifacts_path, 'tmp/uploads/') end def self.artifacts_cache_path - File.expand_path('shared/artifacts/tmp/cache/', Rails.root) + File.join(self.artifacts_path, 'tmp/cache/') end def initialize(build, field) diff --git a/app/views/admin/users/_projects.html.haml b/app/views/admin/users/_projects.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..a126a858ea85f0ce96e785dc2ebc0297a161d38f --- /dev/null +++ b/app/views/admin/users/_projects.html.haml @@ -0,0 +1,13 @@ +- if local_assigns.has_key?(:contributed_projects) && contributed_projects.present? + .panel.panel-default.contributed-projects + .panel-heading Projects contributed to + = render 'shared/projects/list', + projects: contributed_projects.sort_by(&:star_count).reverse, + projects_limit: 5, stars: true, avatar: false + +- if local_assigns.has_key?(:projects) && projects.present? + .panel.panel-default + .panel-heading Personal projects + = render 'shared/projects/list', + projects: projects.sort_by(&:star_count).reverse, + projects_limit: 10, stars: true, avatar: false diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml index 0d7a1a25a8043bef5bb20c2a06ce338c19c5ab46..b655b2a15f5b123dcec41050ee77926de734172b 100644 --- a/app/views/admin/users/projects.html.haml +++ b/app/views/admin/users/projects.html.haml @@ -14,7 +14,7 @@ .row .col-md-6 - if @personal_projects.present? - = render 'users/projects', projects: @personal_projects + = render 'admin/users/projects', projects: @personal_projects - else .nothing-here-block This user has no personal projects. diff --git a/app/views/dashboard/projects/index.atom.builder b/app/views/dashboard/projects/index.atom.builder index d2c5148684131f211bbf7cb5126f8942d7ad8929..c8c219f4cca00870d86877536e60a764431d281c 100644 --- a/app/views/dashboard/projects/index.atom.builder +++ b/app/views/dashboard/projects/index.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html" xml.id dashboard_projects_url - xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? + xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? @events.each do |event| event_to_atom(xml, event) diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index 3c19381321aec37e36924f7c34635726ea265fde..be94b1abc11fdf2de51de541309f1bd224b66b82 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -1,6 +1,5 @@ - user = member.user - return unless user || member.invite? -- show_roles = true if show_roles.nil? %li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)} %span{class: ("list-item-name" if show_controls)} @@ -25,11 +24,11 @@ = link_to member.created_by.name, user_path(member.created_by) = time_ago_with_tooltip(member.created_at) - - if show_controls && can?(current_user, :admin_group_member, member) + - if show_controls && can?(current_user, :admin_group_member, @group) = link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do Resend invite - - if show_roles + - if should_user_see_group_roles?(current_user, @group) %span.pull-right %strong= member.human_access - if show_controls @@ -37,6 +36,7 @@ = button_tag class: "btn-xs btn js-toggle-button", title: 'Edit access level', type: 'button' do %i.fa.fa-pencil-square-o + - if can?(current_user, :destroy_group_member, member)   - if current_user == user diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 15d289471c9a743ac532d4b6b86588d98577971c..d4ad33a8bf144a68373ed24747447ac5219a3f30 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -1,8 +1,6 @@ - page_title "Members" - header_title group_title(@group, "Members", group_group_members_path(@group)) -- show_roles = should_user_see_group_roles?(current_user, @group) - -- if show_roles +- if should_user_see_group_roles?(current_user, @group) %p.light Members of group have access to all group projects. Read more about permissions @@ -32,7 +30,7 @@ (#{@members.total_count}) %ul.well-list - @members.each do |member| - = render 'groups/group_members/group_member', member: member, show_roles: show_roles, show_controls: true + = render 'groups/group_members/group_member', member: member, show_controls: true = paginate @members, theme: 'gitlab' diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder index a91d1a6e94b23c44b5d2fdae571ecdcb2752806b..7ea574434c3b81d3072fda3f794c0db14b7f1da2 100644 --- a/app/views/groups/show.atom.builder +++ b/app/views/groups/show.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: group_url(@group, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: group_url(@group), rel: "alternate", type: "text/html" xml.id group_url(@group) - xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? + xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? @events.each do |event| event_to_atom(xml, event) diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 3ca30d3baabd89114d18b52495d25f56678c08f5..6b850ac5d9c1e6a4e5a396331558854a11052bde 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -15,7 +15,7 @@ = icon('search') - if session[:impersonator_id] %li.impersonation - = link_to stop_impersonation_admin_users_path, method: :delete, title: 'Stop impersonation', data: { toggle: 'tooltip', placement: 'bottom' } do + = link_to admin_impersonation_path, method: :delete, title: 'Stop impersonation', data: { toggle: 'tooltip', placement: 'bottom' } do = icon('user-secret fw') - if current_user.is_admin? %li diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 8c0980369fd867d700b331efa7fc3ed5cc9a3340..88d54bf6f21466da2b6c94374c7d84a317a0e26c 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -18,17 +18,12 @@ .project-repo-buttons .split-one = render 'projects/buttons/star' + = render 'projects/buttons/fork' - - unless empty_repo - = render 'projects/buttons/fork' - = render "shared/clone_panel" - .split-repo-buttons - - unless empty_repo - - if can? current_user, :download_code, @project - = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn', rel: 'nofollow' do - = icon('download fw') - + + .split-repo-buttons + = render "projects/buttons/download" = render 'projects/buttons/dropdown' + = render 'projects/buttons/notifications' - diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml index 63ebfc9381f25bf82a40e5a9fe24904fe8d2cc99..7e6301abde82aca3767f01e850733c78b2e06113 100644 --- a/app/views/projects/_zen.html.haml +++ b/app/views/projects/_zen.html.haml @@ -2,9 +2,12 @@ %input#zen-toggle-comment.zen-toggle-comment(tabindex="-1" type="checkbox") .zen-backdrop - classes << ' js-gfm-input markdown-area' - = f.text_area attr, class: classes, placeholder: '' + - if defined?(f) && f + = f.text_area attr, class: classes, placeholder: '' + - else + = text_area_tag attr, nil, class: classes, placeholder: '' %a.zen-enter-link(tabindex="-1" href="#") - %i.fa.fa-expand + = icon('expand') Edit in fullscreen %a.zen-leave-link(href="#") - %i.fa.fa-compress + = icon('compress') diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml index 373b3a0c5b0afe674cd720fb8bf68c0d8fa6aca0..ba3e0c3c590b1759c8a68f40be3bc106adf52b98 100644 --- a/app/views/projects/blob/_actions.html.haml +++ b/app/views/projects/blob/_actions.html.haml @@ -19,4 +19,4 @@ - if allowed_tree_edit? .btn-group{ role: "group" } %button.btn.btn-default{ 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } Replace - %button.btn.btn-remove{ 'data-target' => '#modal-remove-blob', 'data-toggle' => 'modal' } Remove + %button.btn.btn-remove{ 'data-target' => '#modal-remove-blob', 'data-toggle' => 'modal' } Delete diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml index a0fc8bbd75201e077631c45ed2ce5285b798b1a8..13b5ffd17ff1c69667076a53079568cc8abe20fa 100644 --- a/app/views/projects/blob/_new_dir.html.haml +++ b/app/views/projects/blob/_new_dir.html.haml @@ -5,21 +5,19 @@ %a.close{href: "#", "data-dismiss" => "modal"} × %h3.page-title Create New Directory .modal-body - = form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, id: 'dir-create-form', class: 'form-horizontal' do + = form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form' do .form-group = label_tag :dir_name, 'Directory Name', class: 'control-label' .col-sm-10 = text_field_tag :dir_name, params[:dir_name], placeholder: "Directory name", required: true, class: 'form-control' - = render 'shared/commit_message_container', params: params, placeholder: '' - - unless @project.empty_repo? - .form-group - = label_tag :branch_name, 'Branch', class: 'control-label' - .col-sm-10 - = text_field_tag 'new_branch', @ref, class: "form-control" + + = render 'shared/new_commit_form', placeholder: "Add new directory" + .form-group .col-sm-offset-2.col-sm-10 = submit_tag "Create directory", class: 'btn btn-primary btn-create' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" :javascript - disableButtonIfAnyEmptyField($("#dir-create-form"), ".form-control", ".btn-create"); + disableButtonIfAnyEmptyField($(".js-create-dir-form"), ".form-control", ".btn-create"); + new NewCommitForm($('.js-create-dir-form')) diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml index cae5ff010994f4a1b18b73513612369e49c431c2..1cf19a7d3db916b991afacb9cd684e8e3e1ddc46 100644 --- a/app/views/projects/blob/_remove.html.haml +++ b/app/views/projects/blob/_remove.html.haml @@ -3,16 +3,16 @@ .modal-content .modal-header %a.close{href: "#", "data-dismiss" => "modal"} × - %h3.page-title Remove #{@blob.name} - %p.light - From branch - %strong= @ref + %h3.page-title Delete #{@blob.name} .modal-body - = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-requires-input' do - = render 'shared/commit_message_container', params: params, - placeholder: 'Removed this file because...' + = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-replace-blob-form js-requires-input' do + = render 'shared/new_commit_form', placeholder: "Delete #{@blob.name}" + .form-group .col-sm-offset-2.col-sm-10 - = button_tag 'Remove file', class: 'btn btn-remove btn-remove-file' + = button_tag 'Delete file', class: 'btn btn-remove btn-remove-file' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" + +:javascript + new NewCommitForm($('.js-replace-blob-form')) diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index a1c54e731f04c2553049ca2f78740a77165cc1e9..3bb61f0c9444a90a50f32dccbb90eeaed7521d37 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -5,7 +5,7 @@ %a.close{href: "#", "data-dismiss" => "modal"} × %h3.page-title #{title} .modal-body - = form_tag form_path, method: method, class: 'blob-file-upload-form-js form-horizontal' do + = form_tag form_path, method: method, class: 'js-upload-blob-form form-horizontal' do .dropzone .dropzone-previews.blob-upload-dropzone-previews %p.dz-message.light @@ -13,19 +13,15 @@ = link_to 'click to upload', '#', class: "markdown-selector" %br .dropzone-alerts{class: "alert alert-danger data", style: "display:none"} - = render 'shared/commit_message_container', params: params, - placeholder: placeholder - - unless @project.empty_repo? - .form-group.branch - = label_tag 'branch', class: 'control-label' do - Branch - .col-sm-10 - = text_field_tag 'new_branch', @ref, class: "form-control" + + = render 'shared/new_commit_form', placeholder: placeholder + .form-group .col-sm-offset-2.col-sm-10 = button_tag button_title, class: 'btn btn-small btn-primary btn-upload-file', id: 'submit-all' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" :javascript - disableButtonIfEmptyField($('.blob-file-upload-form-js').find('#commit_message'), '.btn-upload-file'); - new BlobFileDropzone($('.blob-file-upload-form-js'), '#{method}'); + disableButtonIfEmptyField($('.js-upload-blob-form').find('.js-commit-message'), '.btn-upload-file'); + new BlobFileDropzone($('.js-upload-blob-form'), '#{method}'); + new NewCommitForm($('.js-upload-blob-form')) diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index a811adc5094803d3039e1e8742c7a9a1d3881196..56745165251993c750590394f44ce24ecb5e0820 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -13,15 +13,9 @@ %i.fa.fa-eye = editing_preview_title(@blob.name) - = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-requires-input') do + = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-requires-input js-edit-blob-form') do = render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data - = render 'shared/commit_message_container', params: params, placeholder: "Update #{@blob.name}" - - .form-group.branch - = label_tag 'branch', class: 'control-label' do - Branch - .col-sm-10 - = text_field_tag 'new_branch', @ref, class: "form-control" + = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}" = hidden_field_tag 'last_commit', @last_commit = hidden_field_tag 'content', '', id: "file-content" @@ -30,3 +24,4 @@ :javascript blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", "#{@blob.language.try(:ace_mode)}") + new NewCommitForm($('.js-edit-blob-form')) diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index 7975137c37f018f20911f5c9370c474cd3deb303..1ff68005450841e4a3aeea889b633dc9fa6007d2 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -2,20 +2,13 @@ = render "header_title" .gray-content-block.top-block - Create a new file + %h3.page-title + Create New File .file-editor - = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal form-new-file js-requires-input') do + = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-new-blob-form js-requires-input') do = render 'projects/blob/editor', ref: @ref - = render 'shared/commit_message_container', params: params, - placeholder: 'Add new file' - - - unless @project.empty_repo? - .form-group.branch - = label_tag 'branch', class: 'control-label' do - Branch - .col-sm-10 - = text_field_tag 'new_branch', @ref, class: "form-control js-quick-submit" + = render 'shared/new_commit_form', placeholder: "Add new file" = hidden_field_tag 'content', '', id: 'file-content' = render 'projects/commit_button', ref: @ref, @@ -23,3 +16,4 @@ :javascript blob = new NewBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", null) + new NewCommitForm($('.js-new-blob-form')) diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index f52b89f69210f1aff6ded38209e88dd99f782ae5..b7276868ce67da31a75007d4ee7811c74baedb8d 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -10,6 +10,4 @@ = render 'projects/blob/remove' - title = "Replace #{@blob.name}" - = render 'projects/blob/upload', title: title, placeholder: title, - button_title: 'Replace file', form_path: namespace_project_update_blob_path(@project.namespace, @project, @id), - method: :put + = render 'projects/blob/upload', title: title, placeholder: title, button_title: 'Replace file', form_path: namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..14ee2263b7d8a417ed50f2bbaf18a44c39d20704 --- /dev/null +++ b/app/views/projects/buttons/_download.html.haml @@ -0,0 +1,4 @@ +- unless @project.empty_repo? + - if can? current_user, :download_code, @project + = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has_tooltip', rel: 'nofollow', title: "Download ZIP" do + = icon('download') diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 18cae8ef6d348782733f0a1cb96bad9fa9a01a40..b277b765b6b526a0c123e01823c0e9cdd9012f5a 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -32,5 +32,3 @@ = link_to new_namespace_project_tag_path(@project.namespace, @project) do = icon('tags fw') New tag - - diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index 8f2f631eb7d24eeb1ea50f87672ae5d2da46efc9..2d3abf09051852a7bab10bf71923ce8726e86614 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -1,12 +1,13 @@ -- if current_user && can?(current_user, :fork_project, @project) - - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn' do - = icon('code-fork fw') - Fork - %span.count - = @project.forks_count - - else - = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn' do - = icon('code-fork fw') - %span.count - = @project.forks_count +- unless @project.empty_repo? + - if current_user && can?(current_user, :fork_project, @project) + - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn has_tooltip' do + = icon('code-fork fw') + Fork + %span.count + = @project.forks_count + - else + = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has_tooltip' do + = icon('code-fork fw') + %span.count + = @project.forks_count diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml index 06583902035de013d505250661d194a988b7ffe1..41a3ec6d90fafbed9a389e5cd59486ad678c4837 100644 --- a/app/views/projects/buttons/_star.html.haml +++ b/app/views/projects/buttons/_star.html.haml @@ -1,5 +1,5 @@ - if current_user - = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star', method: :post, remote: true do + = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star has_tooltip', method: :post, remote: true, title: "Star project" do = icon('star fw') %span.count = @project.star_count diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 30a3973828ff39cfd7139a8ab5f3899c539d5c64..85e203cbe574f07e49211f0b925cfda1551564e5 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -3,4 +3,4 @@ = render "commit_box" = render "ci_menu" if @ci_commit = render "projects/diffs/diffs", diffs: @diffs, project: @project -= render "projects/notes/notes_with_form", view: params[:view] += render "projects/notes/notes_with_form" diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index e46bf1ab1e7c51a81b95c8084559d7924001f84e..416fb4da071e204fa302c2bcfc4609490ec658fe 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -1,4 +1,4 @@ -- if params[:view] == 'parallel' +- if diff_view == 'parallel' - fluid_layout true - diff_files = safe_diff_files(diffs) diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 410ff6abb43f1f892f1560ed853a8ed65f88efe7..ba4353e5f09ea097b2d6d52170a5d5b41898a099 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -2,8 +2,9 @@ .diff-header{id: "file-path-#{hexdigest(diff_file.file_path)}"} - if diff_file.diff.submodule? %span - - submodule_item = project.repository.blob_at(@commit.id, diff_file.file_path) - = submodule_link(submodule_item, @commit.id, project.repository) + = icon('archive fw') + %strong + = submodule_link(blob, @commit.id, project.repository) - else %span - if diff_file.deleted_file @@ -33,7 +34,7 @@ -# Skipp all non non-supported blobs - return unless blob.respond_to?('text?') - if blob.text? - - if params[:view] == 'parallel' + - if diff_view == 'parallel' = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i - else = render "projects/diffs/text_file", diff_file: diff_file, index: i @@ -42,4 +43,3 @@ = render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i - else .nothing-here-block No preview for this file type - diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml index 92a87690c54992961bdb9bc97b72d39ff97d998b..6027fb23360ffb3904e81f42112442b0438ec6fb 100644 --- a/app/views/projects/imports/new.html.haml +++ b/app/views/projects/imports/new.html.haml @@ -1,22 +1,19 @@ - page_title "Import repository" %h3.page-title - - if @project.import_failed? - Import failed. Retry? - - else - Import repository + Import repository %hr +- if @project.import_failed? + .panel.panel-danger + .panel-heading The repository could not be imported. + .panel-body + %pre + :preserve + #{@project.import_error.try(:strip)} + = form_for @project, url: namespace_project_import_path(@project.namespace, @project), method: :post, html: { class: 'form-horizontal' } do |f| - .form-group.import-url-data - = f.label :import_url, class: 'control-label' do - %span Import existing git repo - .col-sm-10 - = f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git' - .well.prepend-top-20 - This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git. - %br - The import will time out after 4 minutes. For big repositories, use a clone/push combination. - For SVN repositories, check #{link_to "this migrating from SVN doc.", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"} + = render "shared/import_form", f: f + .form-actions = f.submit 'Start import', class: "btn btn-create", tabindex: 4 diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml index 06886d215a3c432a764d872819f08d9cc22d08e5..c0d1ce0d120a6887bc11a652bd082e69d062e784 100644 --- a/app/views/projects/imports/show.html.haml +++ b/app/views/projects/imports/show.html.haml @@ -8,7 +8,7 @@ - else Import in progress. - unless @project.forked? - %p.monospace git clone --bare #{hidden_pass_url(@project.import_url)} + %p.monospace git clone --bare #{@project.safe_import_url} %p Please wait while we import the repository for you. Refresh at will. :javascript new ProjectImport(); diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index c5fd863ae99eacf0ed5c83eb2e27fb3eb28527be..020952dd0016978a95fd8af1c0bfa3b26da2667b 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -7,7 +7,7 @@ = render 'shared/show_aside' -.gray-content-block.second-block +.gray-content-block.second-block.oneline-block .row .col-md-9 .votes-holder.pull-right diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 55ce912829ddee5c3deb50f95904cf34207d0d6d..d7657ee7e4066dd9dd673d5daa0eacbaa4dc2d79 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -29,8 +29,6 @@ .issue-info = "#{issue.to_reference} opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')} by #{link_to_member(@project, issue.author, avatar: false)}".html_safe - - if issue.votes_count > 0 - = render 'votes/votes_inline', votable: issue - if issue.milestone   %span diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index 7e60782ff5b699cb8d70ea5d68a147a5909fd006..cb75bd8c5bae19348e21a029f42abad92bef0580 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -14,8 +14,10 @@ #votes= render 'votes/votes_block', votable: @merge_request = render "projects/merge_requests/show/participants" .col-md-3 - %span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'} - = cross_project_reference(@project, @merge_request) + .input-group.cross-project-reference + %span.slead.has_tooltip{title: 'Cross-project reference'} + = cross_project_reference(@project, @merge_request) + = clipboard_button .row %section.col-md-9 diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index c5234c0618c4d06250ad6b6032cae58cb8bcb0c0..83e8ad11989b684670c44719afeb0630d2a25714 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -34,8 +34,6 @@ .merge-request-info = "##{merge_request.iid} opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')} by #{link_to_member(@project, merge_request.author, avatar: false)}".html_safe - - if merge_request.votes_count > 0 - = render 'votes/votes_inline', votable: merge_request - if merge_request.milestone_id?   %span diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index a02c12f06a8763bd71ecba7facc92a6b28aaa424..c9d1fc3da2125a55e34c111df2d5b2362eb1b698 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -23,7 +23,6 @@ = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user), {}, {class: 'select2', tabindex: 2} - if import_sources_enabled? - .project-import.js-toggle-container .form-group %label.control-label Import project from @@ -82,19 +81,7 @@ %span Any repo by URL .js-toggle-content.hide - .form-group.import-url-data - = f.label :import_url, class: 'control-label' do - %span Git repository URL - .col-sm-10 - = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git' - .well.prepend-top-20 - %ul - %li - The repository must be accessible over HTTP(S). If it is not publicly accessible, you can add authentication information to the URL: https://username:password@gitlab.company.com/group/project.git. - %li - The import will time out after 4 minutes. For big repositories, use a clone/push combination. - %li - To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}. + = render "shared/import_form", f: f .prepend-botton-10 diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 13dfa0a1bb33fc48bda63410892bfb1fa2b998cb..5dd84317e3b56102978c3601a69986713869b8b5 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -1,5 +1,5 @@ = form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form common-note-form gfm-form" }, authenticity_token: true do |f| - = hidden_field_tag :view, params[:view] + = hidden_field_tag :view, diff_view = hidden_field_tag :line_type = note_target_fields(@note) = f.hidden_field :commit_id diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index efa7dd01cc2f9f53d126ab740bfeb4f2b6a89a32..dd0abc8c74681047c18d08761d7f027f0c60852e 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -35,26 +35,6 @@ - if note.updated_by && note.updated_by != note.author by #{link_to_member(note.project, note.updated_by, avatar: false, author_class: nil)} - - if note.superceded?(@notes) - - if note.upvote? - %span.vote.upvote.label.label-gray.strikethrough - = icon('thumbs-up') - \+1 - - if note.downvote? - %span.vote.downvote.label.label-gray.strikethrough - = icon('thumbs-down') - \-1 - - else - - if note.upvote? - %span.vote.upvote.label.label-success - = icon('thumbs-up') - \+1 - - if note.downvote? - %span.vote.downvote.label.label-danger - = icon('thumbs-down') - \-1 - - .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''} .note-text = preserve do diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 04222b8f7c47e10d7e2d3922c3235a6fd6c2bde9..99c1b0fa43eb8135cd5c572d4d8710e17a9e1815 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -4,7 +4,7 @@ .js-main-target-form - if can? current_user, :create_note, @project - = render "projects/notes/form", view: params[:view] + = render "projects/notes/form", view: diff_view :javascript - new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{params[:view]}") + new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml index 76c46d1d8067a7fe7dc855fa09a342c40260dbc6..f07cd97e63d45316913e2d4a981da4ba648dc367 100644 --- a/app/views/projects/project_members/_project_member.html.haml +++ b/app/views/projects/project_members/_project_member.html.haml @@ -24,18 +24,19 @@ = link_to member.created_by.name, user_path(member.created_by) = time_ago_with_tooltip(member.created_at) - - if current_user_can_admin_project + - if can?(current_user, :admin_project_member, @project) = link_to resend_invite_namespace_project_project_member_path(@project.namespace, @project, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do Resend invite - - if current_user_can_admin_project - - unless @project.personal? && user == current_user - .pull-right - %strong= member.human_access + - if can?(current_user, :admin_project_member, @project) + .pull-right + %strong= member.human_access + - if can?(current_user, :update_project_member, member) = button_tag class: "btn-xs btn js-toggle-button", title: 'Edit access level', type: 'button' do %i.fa.fa-pencil-square-o + - if can?(current_user, :destroy_project_member, member)   - if current_user == user = link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: leave_project_message(@project) }, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index 615c425e59ae2967a6ba90f5d7ff6ad8bd83103d..b807fb2cc9dffa65257ec981a93d968436762175 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -1,5 +1,3 @@ -- can_admin_project = can?(current_user, :admin_project, @project) - .panel.panel-default.prepend-top-20 .panel-heading %strong #{@project.name} @@ -8,4 +6,4 @@ (#{members.count}) %ul.well-list - members.each do |project_member| - = render 'project_member', member: project_member, current_user_can_admin_project: can_admin_project + = render 'project_member', member: project_member diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml index 811b1858821c93518116a449322dc0b161954f77..2fb3a41d541051ba1fa06df53f611abb0e60b4c5 100644 --- a/app/views/projects/project_members/update.js.haml +++ b/app/views/projects/project_members/update.js.haml @@ -1,3 +1,2 @@ -- can_admin_project = can?(current_user, :admin_project, @project) :plain - $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render("project_member", member: @project_member, current_user_can_admin_project: can_admin_project))}'); + $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render("project_member", member: @project_member))}'); diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index e7db09cdaa9153b3ec8957ce0950fb8993b9b4c5..f516b65ecd0512fd131f6c06a39cd58ff0488ea9 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -11,10 +11,9 @@ .prepend-top-default = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form release-form' }) do |f| = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', f: f, attr: :description, classes: 'description js-quick-submit' + = render 'projects/zen', f: f, attr: :description, classes: 'description js-quick-submit form-control' = render 'projects/notes/hints' .error-alert .prepend-top-default = f.submit 'Save changes', class: 'btn btn-save' = link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel" - diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder index 242684e5c7cba676d10a4906a0f7c2fc9d0b1856..15c49767556dbf9593da79190cbfcda4e96b6af8 100644 --- a/app/views/projects/show.atom.builder +++ b/app/views/projects/show.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml" xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html" xml.id namespace_project_url(@project.namespace, @project) - xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? + xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? @events.each do |event| event_to_atom(xml, event) diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index e106be794f1a5fd31c0e79c59d69e7afe8f0fa8c..86aa15dc5b3e209bbcd2a8243f1cc140a493a3f6 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -10,7 +10,7 @@ New git tag %hr -= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal tag-form" do += form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form" do .form-group = label_tag :tag_name, 'Name for new tag', class: 'control-label' .col-sm-10 @@ -30,16 +30,7 @@ = label_tag :release_description, 'Release notes', class: 'control-label' .col-sm-10 = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - .zennable - %input#zen-toggle-comment.zen-toggle-comment(tabindex="-1" type="checkbox") - .zen-backdrop - = text_area_tag :release_description, nil, class: 'js-gfm-input markdown-area description js-quick-submit form-control', placeholder: '' - %a.zen-enter-link(tabindex="-1" href="#") - = icon('expand') - Edit in fullscreen - %a.zen-leave-link(href="#") - = icon('compress') - + = render 'projects/zen', attr: :release_description, classes: 'description js-quick-submit form-control' = render 'projects/notes/hints' .help-block (Optional) You can add release notes to your tag. It will be stored in the GitLab database and shown on the tags page .form-actions diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index ee4c9d1693d1c7ded187ef654ea979e090e7fe55..c64e684df262f25c8777c8b082e1cce5fc035ba4 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -30,7 +30,7 @@ = render "projects/tree/readme", readme: tree.readme - if allowed_tree_edit? - = render 'projects/blob/upload', title: 'Upload', placeholder: 'Upload new file', button_title: 'Upload file', form_path: namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post + = render 'projects/blob/upload', title: 'Upload New File', placeholder: 'Upload new file', button_title: 'Upload file', form_path: namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post = render 'projects/blob/new_dir' :javascript diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 2e4aab36301833c1fa63becd42024df25f5dd7f1..8bcb24ae9dff6a683dcc3a16416be9b9049bb38d 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -21,7 +21,6 @@ = gitlab_config.protocol.upcase = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true - if project.kind_of?(Project) - .input-group-addon - .visibility-level-label.has_tooltip{'data-title' => "#{visibility_level_label(project.visibility_level)} project" } + .input-group-addon.has_tooltip{title: "#{visibility_level_label(project.visibility_level)} project", data: { container: "body" } } + .visibility-level-label = visibility_level_icon(project.visibility_level) - diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml index cc3f1268f8b9fb8d01dcc87ecc3dcee30c51646b..7c57924277e853f82b86408eb8698ad60b336e3d 100644 --- a/app/views/shared/_commit_message_container.html.haml +++ b/app/views/shared/_commit_message_container.html.haml @@ -1,13 +1,15 @@ .form-group.commit_message-group - = label_tag 'commit_message', class: 'control-label' do + - nonce = SecureRandom.hex + = label_tag "commit_message-#{nonce}", class: 'control-label' do Commit message .col-sm-10 .commit-message-container .max-width-marker = text_area_tag 'commit_message', (params[:commit_message] || local_assigns[:text]), - class: 'form-control js-quick-submit', placeholder: local_assigns[:placeholder], - required: true, rows: (local_assigns[:rows] || 3) + class: 'form-control js-commit-message js-quick-submit', placeholder: local_assigns[:placeholder], + required: true, rows: (local_assigns[:rows] || 3), + id: "commit_message-#{nonce}" - if local_assigns[:hint] %p.hint Try to keep the first line under 52 characters diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..285af56ad73c28bcf4a30a80c974669377138df7 --- /dev/null +++ b/app/views/shared/_import_form.html.haml @@ -0,0 +1,16 @@ +.form-group.import-url-data + = f.label :import_url, class: 'control-label' do + %span Git repository URL + .col-sm-10 + = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git' + + .well.prepend-top-20 + %ul + %li + The repository must be accessible over http://, https:// or git://. + %li + If your HTTP repository is not publicly accessible, add authentication information to the URL: https://username:password@gitlab.company.com/group/project.git. + %li + The import will time out after 4 minutes. For big repositories, use a clone/push combination. + %li + To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}. diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..8636341c60dbe99f177c5d0be368bb40de421ede --- /dev/null +++ b/app/views/shared/_new_commit_form.html.haml @@ -0,0 +1,18 @@ += render 'shared/commit_message_container', placeholder: placeholder + +- unless @project.empty_repo? + .form-group.branch + = label_tag 'branch', class: 'control-label' do + Branch + .col-sm-10 + = text_field_tag 'new_branch', @new_branch || @ref, class: "form-control js-new-branch" + + .form-group.js-create-merge-request-form-group + .col-sm-offset-2.col-sm-10 + .checkbox + - nonce = SecureRandom.hex + = label_tag "create_merge_request-#{nonce}" do + = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}" + Start a new merge request with this commit + + = hidden_field_tag 'original_branch', @ref, class: 'js-original-branch' diff --git a/app/views/users/show.atom.builder b/app/views/users/show.atom.builder index 50232dc7186054179c5a5477948e86507079819c..2fe5b7fac8327ff384fd28bb8cc39b405269bf09 100644 --- a/app/views/users/show.atom.builder +++ b/app/views/users/show.atom.builder @@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml" xml.link href: user_url(@user), rel: "alternate", type: "text/html" xml.id user_url(@user) - xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? + xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any? @events.each do |event| event_to_atom(xml, event) diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml index 36ea67420641ee8af1d63bf450107dc4e56f00ba..7eb27c12d3395028474f9cb969367e500be3f85b 100644 --- a/app/views/votes/_votes_block.html.haml +++ b/app/views/votes/_votes_block.html.haml @@ -1,10 +1,32 @@ -.votes.votes-block - .btn-group - - unless votable.upvotes.zero? - .btn.btn-sm.disabled.cgreen - %i.fa.fa-thumbs-up - = votable.upvotes - - unless votable.downvotes.zero? - .btn.btn-sm.disabled.cred - %i.fa.fa-thumbs-down - = votable.downvotes +.awards.votes-block + - votable.notes.awards.grouped_awards.each do |emoji, notes| + .award{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user)} + .icon{"data-emoji" => "#{emoji}"} + = image_tag url_to_emoji(emoji), height: "20px", width: "20px" + .counter + = notes.count + + - if current_user + .dropdown.awards-controls + %a.add-award{"data-toggle" => "dropdown", "data-target" => "#", "href" => "#"} + = icon('smile-o') + %ul.dropdown-menu.awards-menu + - emoji_list.each do |emoji| + %li{"data-emoji" => "#{emoji}"}= image_tag url_to_emoji(emoji), height: "20px", width: "20px" + +- if current_user + :coffeescript + post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}" + noteable_type = "#{votable.class.name.underscore}" + noteable_id = "#{votable.id}" + window.awards_handler = new AwardsHandler(post_emoji_url, noteable_type, noteable_id) + + $(".awards-menu li").click (e)-> + emoji = $(this).data("emoji") + awards_handler.addAward(emoji) + + $(".awards").on "click", ".award", (e)-> + emoji = $(this).find(".icon").data("emoji") + awards_handler.addAward(emoji) + + $(".award").tooltip() diff --git a/app/views/votes/_votes_inline.html.haml b/app/views/votes/_votes_inline.html.haml deleted file mode 100644 index 2cb3ae04e1a83b670b824993ee5a59e1dcf29684..0000000000000000000000000000000000000000 --- a/app/views/votes/_votes_inline.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -.votes.votes-inline - - unless votable.upvotes.zero? - %span.upvotes.cgreen - + #{votable.upvotes} - - unless votable.downvotes.zero? - \/ - - unless votable.downvotes.zero? - %span.downvotes.cred - \- #{votable.downvotes} diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index acd1c43f06b7d4e33277c0cca834fbf13280c20c..2f991c523390af1ab894a067b9a6f3ccd8921cd4 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -13,22 +13,20 @@ class RepositoryForkWorker end result = gitlab_shell.fork_repository(source_path, target_path) - unless result logger.error("Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}") + project.update(import_error: "The project could not be forked.") project.import_fail - project.save return end - if project.valid_repo? - ProjectCacheWorker.perform_async(project.id) - project.import_finish - else - project.import_fail + unless project.valid_repo? logger.error("Project #{id} had an invalid repository after fork") + project.update(import_error: "The forked repository is invalid.") + project.import_fail + return end - project.save + project.import_finish end end diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index ea2808045eb2e93c70202b089f1d82f1423c0338..d18c0706b303d7722585914633d43ac8d0aaed0f 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -7,37 +7,49 @@ class RepositoryImportWorker def perform(project_id) project = Project.find(project_id) - unless project.import_url == Project::UNKNOWN_IMPORT_URL - import_result = gitlab_shell.send(:import_repository, - project.path_with_namespace, - project.import_url) - return project.import_fail unless import_result - else + if project.import_url == Project::UNKNOWN_IMPORT_URL + # In this case, we only want to import issues, not a repository. unless project.create_repository - return project.import_fail + project.update(import_error: "The repository could not be created.") + project.import_fail + return + end + else + begin + gitlab_shell.import_repository(project.path_with_namespace, project.import_url) + rescue Gitlab::Shell::Error => e + project.update(import_error: e.message) + project.import_fail + return end end - data_import_result = case project.import_type - when 'github' - Gitlab::GithubImport::Importer.new(project).execute - when 'gitlab' - Gitlab::GitlabImport::Importer.new(project).execute - when 'bitbucket' - Gitlab::BitbucketImport::Importer.new(project).execute - when 'google_code' - Gitlab::GoogleCodeImport::Importer.new(project).execute - when 'fogbugz' - Gitlab::FogbugzImport::Importer.new(project).execute - else - true - end - return project.import_fail unless data_import_result + data_import_result = + case project.import_type + when 'github' + Gitlab::GithubImport::Importer.new(project).execute + when 'gitlab' + Gitlab::GitlabImport::Importer.new(project).execute + when 'bitbucket' + Gitlab::BitbucketImport::Importer.new(project).execute + when 'google_code' + Gitlab::GoogleCodeImport::Importer.new(project).execute + when 'fogbugz' + Gitlab::FogbugzImport::Importer.new(project).execute + else + true + end - Gitlab::BitbucketImport::KeyDeleter.new(project).execute if project.import_type == 'bitbucket' + unless data_import_result + project.update(import_error: "The remote issue data could not be imported.") + project.import_fail + return + end + + if project.import_type == 'bitbucket' + Gitlab::BitbucketImport::KeyDeleter.new(project).execute + end project.import_finish - project.save - ProjectCacheWorker.perform_async(project.id) end end diff --git a/app/workers/stuck_ci_builds_worker.rb b/app/workers/stuck_ci_builds_worker.rb index ad02a3b16d953b46eb5901e2be171a2211aa4382..4e5eddbaba1c68a8de1717eac0dab0f075544c56 100644 --- a/app/workers/stuck_ci_builds_worker.rb +++ b/app/workers/stuck_ci_builds_worker.rb @@ -14,5 +14,8 @@ class StuckCiBuildsWorker Rails.logger.debug "Dropping stuck #{build.status} build #{build.id} for runner #{build.runner_id}" build.drop end + + # Update builds that failed to drop + builds.update_all(status: 'failed') end end diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 8fdb2603ce8f1d86eb299814beb37028721c0d3f..1da42ab38f30ba27b03b9fd54b117e67a4a58d74 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -124,9 +124,15 @@ production: &base # The mailbox where incoming mail will end up. Usually "inbox". mailbox: "inbox" + ## Build Artifacts + artifacts: + enabled: true + # The location where build artifacts are stored (default: shared/artifacts). + # path: shared/artifacts + ## Git LFS lfs: - enabled: false + enabled: true # The location where LFS objects are stored (default: shared/lfs-objects). # storage_path: shared/lfs-objects diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 6b7990c0ab06f8f703bf5e57de35ca18bf6cc9c9..444e91109dff104898ba9c788a0cab2634d9ddf9 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -187,7 +187,6 @@ Settings.gitlab_ci['all_broken_builds'] = true if Settings.gitlab_ci['all_br Settings.gitlab_ci['add_pusher'] = false if Settings.gitlab_ci['add_pusher'].nil? Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url) Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_ci['builds_path'] || "builds/", Rails.root) -Settings.gitlab_ci['max_artifacts_size'] ||= 100 # in megabytes # # Reply by email @@ -199,11 +198,19 @@ Settings.incoming_email['ssl'] = false if Settings.incoming_email['ssl']. Settings.incoming_email['start_tls'] = false if Settings.incoming_email['start_tls'].nil? Settings.incoming_email['mailbox'] = "inbox" if Settings.incoming_email['mailbox'].nil? +# +# Build Artifacts +# +Settings['artifacts'] ||= Settingslogic.new({}) +Settings.artifacts['enabled'] = true if Settings.artifacts['enabled'].nil? +Settings.artifacts['path'] = File.expand_path(Settings.artifacts['path'] || File.join(Settings.shared['path'], "artifacts"), Rails.root) +Settings.artifacts['max_size'] ||= 100 # in megabytes + # # Git LFS # Settings['lfs'] ||= Settingslogic.new({}) -Settings.lfs['enabled'] = false if Settings.lfs['enabled'].nil? +Settings.lfs['enabled'] = true if Settings.lfs['enabled'].nil? Settings.lfs['storage_path'] = File.expand_path(Settings.lfs['storage_path'] || File.join(Settings.shared['path'], "lfs-objects"), Rails.root) # @@ -286,3 +293,12 @@ if Rails.env.test? Settings.gitlab['default_can_create_group'] = true Settings.gitlab['default_can_create_team'] = false end + +# Force a refresh of application settings at startup +begin + ApplicationSetting.expire + Ci::ApplicationSetting.expire +rescue + # Gracefully handle when Redis is not available. For example, + # omnibus may fail here during assets:precompile. +end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 29506970af24bfb5c4494d705e847fa7580e746a..5fb43a86e13d385f5831108859636a28bce56384 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -60,7 +60,7 @@ Devise.setup do |config| # It will change confirmation, password recovery and other workflows # to behave the same regardless if the e-mail provided was right or wrong. # Does not affect registerable. - # config.paranoid = true + config.paranoid = true # ==> Configuration for :database_authenticatable # For bcrypt, this is the cost for hashing the password and defaults to 10. If diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index d7c5432da76553e310ab3ae7dcdc9ac827a8c425..f30178ff7117aab5f1cb4446ac7778d609b8e10a 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -3,10 +3,11 @@ require 'gitlab/current_settings' include Gitlab::CurrentSettings -# allow it to fail: it may to do so when create_from_defaults is executed before migrations are actually done +# allow it to fail: it may do so when create_from_defaults is executed before migrations are actually done begin - Settings.gitlab['session_expire_delay'] = current_application_settings.session_expire_delay + Settings.gitlab['session_expire_delay'] = current_application_settings.session_expire_delay || 10080 rescue + Settings.gitlab['session_expire_delay'] ||= 10080 end unless Rails.env.test? diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index 22070e37f07f3b1d76df813784e2474fffbf06c8..bd4c3ebc69ec163f497e6c113bba50c94d7b7a24 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -30,7 +30,6 @@ en: success: "Successfully authenticated from %{kind} account." passwords: no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." - recently_reset: "Instructions about how to reset your password have already been sent recently. Please wait a few minutes to try again." send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." updated: "Your password has been changed successfully. You are now signed in." diff --git a/config/routes.rb b/config/routes.rb index 0bc2c17345337bdec28ba6422dc54dba881d8dd4..d3b93894b32cb76d2e52084f1568daeb18940455 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -222,8 +222,6 @@ Gitlab::Application.routes.draw do resources :keys, only: [:show, :destroy] resources :identities, only: [:index, :edit, :update, :destroy] - delete 'stop_impersonation' => 'impersonation#destroy', on: :collection - member do get :projects get :keys @@ -233,12 +231,14 @@ Gitlab::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 :applications @@ -368,7 +368,7 @@ Gitlab::Application.routes.draw do end resource :avatar, only: [:destroy] - resources :milestones, only: [:index, :show, :update, :new, :create] + resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create] end end @@ -664,6 +664,10 @@ Gitlab::Application.routes.draw do member do delete :delete_attachment end + + collection do + post :award_toggle + end end resources :uploads, only: [:create] do diff --git a/db/migrate/20151106000015_add_is_award_to_notes.rb b/db/migrate/20151106000015_add_is_award_to_notes.rb new file mode 100644 index 0000000000000000000000000000000000000000..02b271637e960c9214481e0e4eed4287f49d81c0 --- /dev/null +++ b/db/migrate/20151106000015_add_is_award_to_notes.rb @@ -0,0 +1,6 @@ +class AddIsAwardToNotes < ActiveRecord::Migration + def change + add_column :notes, :is_award, :boolean, default: false, null: false + add_index :notes, :is_award + end +end diff --git a/db/migrate/20151110125604_add_import_error_to_project.rb b/db/migrate/20151110125604_add_import_error_to_project.rb new file mode 100644 index 0000000000000000000000000000000000000000..7fc990f8d0a9f56316c566ae421b158e8d26d268 --- /dev/null +++ b/db/migrate/20151110125604_add_import_error_to_project.rb @@ -0,0 +1,5 @@ +class AddImportErrorToProject < ActiveRecord::Migration + def change + add_column :projects, :import_error, :text + end +end diff --git a/db/migrate/20151118162244_add_projects_public_index.rb b/db/migrate/20151118162244_add_projects_public_index.rb new file mode 100644 index 0000000000000000000000000000000000000000..fded70e3c0c8c38c67f7d30e4be82d11039be991 --- /dev/null +++ b/db/migrate/20151118162244_add_projects_public_index.rb @@ -0,0 +1,5 @@ +class AddProjectsPublicIndex < ActiveRecord::Migration + def change + add_index :namespaces, :public + end +end diff --git a/db/schema.rb b/db/schema.rb index aa76cef9fe4c2b6b72f3ea718a2872df8a072ab8..80e9405f6f136c7ea3e5b2b700cc98e8b7d02fbb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20151116144118) do +ActiveRecord::Schema.define(version: 20151118162244) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -538,6 +538,7 @@ ActiveRecord::Schema.define(version: 20151116144118) do add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree + add_index "namespaces", ["public"], name: "index_namespaces_on_public", using: :btree add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree create_table "notes", force: true do |t| @@ -554,12 +555,14 @@ ActiveRecord::Schema.define(version: 20151116144118) do t.boolean "system", default: false, null: false t.text "st_diff" t.integer "updated_by_id" + t.boolean "is_award", default: false, null: false end add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree add_index "notes", ["created_at", "id"], name: "index_notes_on_created_at_and_id", using: :btree add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree + add_index "notes", ["is_award"], name: "index_notes_on_is_award", using: :btree add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree @@ -640,7 +643,10 @@ ActiveRecord::Schema.define(version: 20151116144118) do t.integer "star_count", default: 0, null: false t.string "import_type" t.string "import_source" - t.integer "commit_count", default: 0 + t.integer "commit_count", default: 0 + t.boolean "merge_requests_ff_only_enabled", default: false + t.text "issues_template" + t.text "import_error" end add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree diff --git a/doc/README.md b/doc/README.md index 0f6866475f716b77b8f7a095f6af0db6d7c78661..58ab5dd08e0b2771a84285e65762d2dead3122a4 100644 --- a/doc/README.md +++ b/doc/README.md @@ -50,6 +50,7 @@ - [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page. - [Reply by email](incoming_email/README.md) Allow users to comment on issues and merge requests by replying to notification emails. - [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE. +- [Git LFS configuration](workflow/lfs/lfs_administration.md) ## Contributor documentation diff --git a/doc/api/README.md b/doc/api/README.md index 6b8528de50c633f72ebd1d0fd314f96110bf0d04..25a31b235cc77dc195bea89b70e02a7c106b34c9 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -10,6 +10,7 @@ - [Repositories](repositories.md) - [Repository Files](repository_files.md) - [Commits](commits.md) +- [Tags](tags.md) - [Branches](branches.md) - [Merge Requests](merge_requests.md) - [Issues](issues.md) diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index ffa7f2cdf14d388d6c1bfaff2e3d73863168c73a..0cef09d5b271fd42d2f621d9096ae3a0b394756f 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -3,8 +3,9 @@ ## List merge requests Get all merge requests for this project. -The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, or `merged`) or all of them (`all`). -The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests. +The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, or `merged`) or all of them (`all`). +The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests. With GitLab 8.2 the return fields `upvotes` and +`downvotes` are deprecated and always return `0`. ``` GET /projects/:id/merge_requests @@ -57,7 +58,7 @@ Parameters: ## Get single MR -Shows information about a single merge request. +Shows information about a single merge request. With GitLab 8.2 the return fields `upvotes` and `downvotes` are deprecated and always return `0`. ``` GET /projects/:id/merge_request/:merge_request_id @@ -102,7 +103,9 @@ Parameters: ## Get single MR changes -Shows information about the merge request including its files and changes +Shows information about the merge request including its files and changes. +With GitLab 8.2 the return fields `upvotes` and `downvotes` are deprecated and +always return `0`. ``` GET /projects/:id/merge_request/:merge_request_id/changes @@ -173,7 +176,8 @@ Parameters: ## Create MR -Creates a new merge request. +Creates a new merge request. With GitLab 8.2 the return fields `upvotes` and ` +downvotes` are deprecated and always return `0`. ``` POST /projects/:id/merge_requests @@ -225,7 +229,8 @@ If an error occurs, an error number and a message explaining the reason is retur ## Update MR -Updates an existing merge request. You can change the target branch, title, or even close the MR. +Updates an existing merge request. You can change the target branch, title, or even close the MR. With GitLab 8.2 the return fields `upvotes` and `downvotes` +are deprecated and always return `0`. ``` PUT /projects/:id/merge_request/:merge_request_id @@ -276,7 +281,8 @@ If an error occurs, an error number and a message explaining the reason is retur ## Accept MR -Merge changes submitted with MR using this API. +Merge changes submitted with MR using this API. With GitLab 8.2 the return +fields `upvotes` and `downvotes` are deprecated and always return `0`. If merge success you get `200 OK`. diff --git a/doc/api/notes.md b/doc/api/notes.md index c683cb883d42505995168da24aafda61654293a8..e7f299c099417eecb30d0434a4b7f85c19d9052e 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -6,7 +6,8 @@ Notes are comments on snippets, issues or merge requests. ### List project issue notes -Gets a list of all notes for a single issue. +Gets a list of all notes for a single issue. With GitLab 8.2 the return fields +`upvote` and `downvote` are deprecated and always return `false`. ``` GET /projects/:id/issues/:issue_id/notes @@ -49,7 +50,7 @@ Parameters: "created_at": "2013-09-30T13:46:01Z" }, "created_at": "2013-10-02T09:56:03Z", - "system": false, + "system": true, "upvote": false, "downvote": false } diff --git a/doc/api/tags.md b/doc/api/tags.md index b5b90cf6b82881fba013b30a9a7a700e9abc7aa6..085d387e824240c39f3aec0410c385dd4a80321d 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -29,7 +29,7 @@ Parameters: ] }, "release": { - "tag": "1.0.0", + "tag_name": "1.0.0", "description": "Amazing release. Wow" }, "name": "v1.0.0", @@ -70,7 +70,7 @@ Parameters: ] }, "release": { - "tag": "1.0.0", + "tag_name": "1.0.0", "description": "Amazing release. Wow" }, "name": "v1.0.0", @@ -84,23 +84,48 @@ It returns 200 if the operation succeed. In case of an error, 405 with an explaining error message is returned. -## New release +## Create a new release -Add release notes to the existing git tag +Add release notes to the existing git tag. It returns 201 if the release is +created successfully. If the tag does not exist, 404 is returned. If there +already exists a release for the given tag, 409 is returned. ``` -PUT /projects/:id/repository/:tag/release +POST /projects/:id/repository/tags/:tag_name/release ``` Parameters: - `id` (required) - The ID of a project -- `tag` (required) - The name of a tag +- `tag_name` (required) - The name of a tag - `description` (required) - Release notes with markdown support ```json { - "tag": "1.0.0", + "tag_name": "1.0.0", "description": "Amazing release. Wow" } ``` + +## Update a release + +Updates the release notes of a given release. It returns 200 if the release is +successfully updated. If the tag or the release does not exist, it returns 404 +with a proper error message. + +``` +PUT /projects/:id/repository/tags/:tag_name/release +``` + +Parameters: + +- `id` (required) - The ID of a project +- `tag_name` (required) - The name of a tag +- `description` (required) - Release notes with markdown support + +```json +{ + "tag_name": "1.0.0", + "description": "Amazing release. Wow" +} +``` \ No newline at end of file diff --git a/doc/install/installation.md b/doc/install/installation.md index 7ef46b0406539d674944ba7596005ac7042a9cf3..2e563fe5d2bf152462c45d700b3cb478661768a2 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -75,7 +75,7 @@ Make sure you have the right version of Git installed # Install Git sudo apt-get install -y git-core - # Make sure Git is version 1.7.10 or higher, for example 1.7.12 or 2.0.0 + # Make sure Git is version 2.7.4 or higher git --version Is the system packaged Git too old? Remove it and compile from source. @@ -88,8 +88,9 @@ Is the system packaged Git too old? Remove it and compile from source. # Download and compile from source cd /tmp - curl -L --progress https://www.kernel.org/pub/software/scm/git/git-2.4.3.tar.gz | tar xz - cd git-2.4.3/ + curl -O --progress https://www.kernel.org/pub/software/scm/git/git-2.7.4.tar.gz + echo '7104c4f5d948a75b499a954524cb281fe30c6649d8abe20982936f75ec1f275b git-2.7.4.tar.gz' | shasum -a256 -c - && tar -xzf git-2.7.4.tar.gz + cd git-2.7.4/ ./configure make prefix=/usr/local all @@ -312,7 +313,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da GitLab Shell is an SSH access and repository management software developed specially for GitLab. # Run the installation task for gitlab-shell (replace `REDIS_URL` if needed): - sudo -u git -H bundle exec rake gitlab:shell:install[v2.6.6] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production + sudo -u git -H bundle exec rake gitlab:shell:install REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production # By default, the gitlab-shell config is generated from your main GitLab config. # You can review (and modify) the gitlab-shell config as follows: @@ -327,12 +328,12 @@ GitLab Shell is an SSH access and repository management software developed speci cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git cd gitlab-workhorse - sudo -u git -H git checkout 0.4.1 + sudo -u git -H git checkout 0.4.2 sudo -u git -H make ### Initialize Database and Activate Advanced Features - # Go to Gitlab installation folder + # Go to GitLab installation folder cd /home/git/gitlab diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index 8d4c2ceab7d25c4a25623010d87ab5487bff40b1..bcd00cfc6bf82f44c19fdc96980a6c19377bbfa7 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -6,6 +6,9 @@ If a user is both in a project group and in the project itself, the highest perm If a user is a GitLab administrator they receive all permissions. +On public projects the Guest role is not enforced. +All users will be able to create issues, leave comments, and pull or download the project code. + To add or import a user, you can follow the [project users and members documentation](doc/workflow/add-user/add-user.md). @@ -15,8 +18,8 @@ documentation](doc/workflow/add-user/add-user.md). |---------------------------------------|---------|------------|-------------|----------|--------| | Create new issue | ✓ | ✓ | ✓ | ✓ | ✓ | | Leave comments | ✓ | ✓ | ✓ | ✓ | ✓ | -| Pull project code | ✓ | ✓ | ✓ | ✓ | ✓ | -| Download project | ✓ | ✓ | ✓ | ✓ | ✓ | +| Pull project code | | ✓ | ✓ | ✓ | ✓ | +| Download project | | ✓ | ✓ | ✓ | ✓ | | Create code snippets | | ✓ | ✓ | ✓ | ✓ | | Manage issue tracker | | ✓ | ✓ | ✓ | ✓ | | Manage labels | | ✓ | ✓ | ✓ | ✓ | diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 1a5442cdac71838425065f70724fd469b7020010..4e645b21a85a3619c2227f506226901fb4dfdf05 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -29,7 +29,7 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ``` Also you can choose what should be backed up by adding environment variable SKIP. Available options: db, -uploads (attachments), repositories, builds(CI build output logs), artifacts (CI build artifacts). +uploads (attachments), repositories, builds(CI build output logs), artifacts (CI build artifacts), lfs (LFS objects). Use a comma to specify several options at the same time. ``` diff --git a/doc/update/8.1-to-8.2.md b/doc/update/8.1-to-8.2.md index 73d899f9c2eddcfb7eb852fd2777b2bdabd3c599..7b228d6a22fcfd82e644e9cb6407952ab01175fd 100644 --- a/doc/update/8.1-to-8.2.md +++ b/doc/update/8.1-to-8.2.md @@ -68,7 +68,7 @@ sudo -u git -H git checkout 8-2-stable-ee ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch -sudo -u git -H git checkout v2.6.6 +sudo -u git -H git checkout v2.6.8 ``` ### 5. Replace gitlab-git-http-server with gitlab-workhorse @@ -81,7 +81,7 @@ from GitLab 8.1. cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git cd gitlab-workhorse -sudo -u git -H git checkout 0.4.1 +sudo -u git -H git checkout 0.4.2 sudo -u git -H make ``` diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index 7d838187a265fd7b2889e76409105ea70440ae66..03746dd9df37a89a1c18873b15ee49709c7a4a4d 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -57,6 +57,9 @@ X-Gitlab-Event: Push Hook "name": "Jordi Mallach", "email": "jordi@softcatala.org" } + "added": ["CHANGELOG"], + "modified": ["app/controller/application.rb"], + "removed": [] }, { "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", @@ -66,13 +69,14 @@ X-Gitlab-Event: Push Hook "author": { "name": "GitLab dev user", "email": "gitlabdev@dv6700.(none)" - } + }, + "added": ["CHANGELOG"], + "modified": ["app/controller/application.rb"], + "removed": [] } ], - "total_commits_count": 4, - "added": ["CHANGELOG"], - "modified": ["app/controller/application.rb"], - "removed": [] + "total_commits_count": 4 + } ``` diff --git a/doc/workflow/README.md b/doc/workflow/README.md index c1a4f64981f8149d28973e52b9fb8fd26f8e3d21..a6b4d9511884f8c1f1a9cb21389bbd0cf7e91a62 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -17,3 +17,4 @@ - [Milestones](milestones.md) - [Merge Requests](merge_requests.md) - ["Work In Progress" Merge Requests](wip_merge_requests.md) +- [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md) diff --git a/doc/workflow/award_emoji.png b/doc/workflow/award_emoji.png new file mode 100644 index 0000000000000000000000000000000000000000..fb26ee043930b2a06f4b93dd3918c041e2f32033 Binary files /dev/null and b/doc/workflow/award_emoji.png differ diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index 31495bce76e10ef6c755a7015f1ee4d2a937ef02..8965e5b365442da9e3a0a4d3f30760eedecd5c10 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -244,13 +244,12 @@ Developing software happen in small messy steps and it is OK to have your histor You can use tools to view the network graphs of commits and understand the messy history that created your code. If you rebase code the history is incorrect, and there is no way for tools to remedy this because they can't deal with changing commit identifiers. -## Voting on merge requests +## Award emojis on issues and merge requests -![Voting slider in GitLab](voting_slider.png) +![Emoji bar in GitLab](award_emoji.png) -It is common to voice approval or disapproval by using +1 or -1 emoticons. -In GitLab the +1 and -1 are aggregated and shown at the top of the merge request. -As a rule of thumb anything that doesn't have two times more +1's than -1's is suspect and should not be merged yet. +It is common to voice approval or disapproval by using +1 or -1. In GitLab you +can use emojis to give a virtual high five on issues and merge requests. ## Pushing and removing branches diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md new file mode 100644 index 0000000000000000000000000000000000000000..5076b2697a355010a8e2350643627f05d6bf415a --- /dev/null +++ b/doc/workflow/lfs/lfs_administration.md @@ -0,0 +1,41 @@ +# GitLab Git LFS Administration + +Documentation on how to use Git LFS are under [Managing large binary files with Git LFS doc](manage_large_binaries_with_git_lfs.md). + +## Requirements + +* Git LFS is supported in GitLab starting with version 8.2. +* Users need to install [Git LFS client](https://git-lfs.github.com) version 1.0.1 and up. + +## Configuration + +Git LFS objects can be large in size. By default, they are stored on the server GitLab is installed on. + +There are two configuration options to help GitLab server administrators: + +* Enabling/disabling Git LFS support +* Changing the location of LFS object storage + +### Omnibus packages + +In `/etc/gitlab/gitlab.rb`: + +```ruby +gitlab_rails['lfs_enabled'] = false +gitlab_rails['lfs_storage_path'] = "/mnt/storage/lfs-objects" +``` + +### Installations from source + +In `config/gitlab.yml`: + +```yaml + lfs: + enabled: false + storage_path: /mnt/storage/lfs-objects +``` + +## Known limitations + +* Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets) is not supported +* Currently, removing LFS objects from GitLab Git LFS storage is not supported diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md new file mode 100644 index 0000000000000000000000000000000000000000..210a8f71c3b1668eab6846845acd2992a4762cc3 --- /dev/null +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -0,0 +1,126 @@ +# Git LFS + +Managing large files such as audio, video and graphics files has always been one of the shortcomings of Git. +The general recommendation is to not have Git repositories larger than 1GB to preserve performance. + +GitLab already supports [managing large files with git annex](http://doc.gitlab.com/ee/workflow/git_annex.html) (EE only), however in certain +environments it is not always convenient to use different commands to differentiate between the large files and regular ones. + +Git LFS makes this simpler for the end user by removing the requirement to learn new commands. + +## How it works + +Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication to authorize client requests. +Once the request is authorized, Git LFS client receives instructions from where to fetch or where to push the large file. + +## GitLab server configuration + +Documentation for GitLab instance administrators is under [LFS administration doc](lfs_administration.md). + +## Requirements + +* Git LFS is supported in GitLab starting with version 8.2 +* [Git LFS client](https://git-lfs.github.com) version 1.0.1 and up + +## Known limitations + +* Git LFS v1 original API is not supported since it was deprecated early in LFS development +* When SSH is set as a remote, Git LFS objects still go through HTTPS +* Any Git LFS request will ask for HTTPS credentials to be provided so good Git credentials store is recommended +* Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have to add the URL to Git config manually (see #troubleshooting) + +## Using Git LFS + +Lets take a look at the workflow when you need to check large files into your Git repository with Git LFS: +For example, if you want to upload a very large file and check it into your Git repository: + +```bash +git clone git@gitlab.example.com:group/project.git +git lfs init # initialize the Git LFS project project +git lfs track "*.iso" # select the file extensions that you want to treat as large files +``` + +Once a certain file extension is marked for tracking as a LFS object you can use Git as usual without having to redo the command to track a file with the same extension: + +```bash +cp ~/tmp/debian.iso ./ # copy a large file into the current directory +git add . # add the large file to the project +git commit -am "Added Debian iso" # commit the file meta data +git push origin master # sync the git repo and large file to the GitLab server +``` + +Cloning the repository works the same as before. Git automatically detects the LFS-tracked files and clones them via HTTP. If you performed the git clone command with a SSH URL, you have to enter your GitLab credentials for HTTP authentication. + +```bash +git clone git@gitlab.example.com:group/project.git +``` + +If you already cloned the repository and you want to get the latest LFS object that are on the remote repository, eg. from branch `master`: + +```bash +git lfs fetch master +``` + +## Troubleshooting + +### error: Repository or object not found + +There are a couple of reasons why this error can occur: + +* You don't have permissions to access certain LFS object + +Check if you have permissions to push to the project or fetch from the project. + +* Project is not allowed to access the LFS object + +LFS object you are trying to push to the project or fetch from the project is not available to the project anymore. +Probably the object was removed from the server. + +* Local git repository is using deprecated LFS API + +### Invalid status for : 501 + +Git LFS will log the failures into a log file. +To view this log file, while in project directory: + +```bash +git lfs logs last +``` + +If the status `error 501` is shown, it is because: + +* Git LFS support is not enabled on the GitLab server. Check with your GitLab administrator why Git LFS is not enabled on the server. See [LFS administration documentation](lfs_administration.md) for instructions on how to enable LFS support. + +* Git LFS client version is not supported by GitLab server. Check your Git LFS version with `git lfs version`. Check the Git config of the project for traces of deprecated API with `git lfs -l`. If `batch = false` is set in the config, remove the line and try to update your Git LFS client. Only version 1.0.1 and newer are supported. + +### getsockopt: connection refused + +If you push a LFS object to a project and you receive an error similar to: `Post /info/lfs/objects/batch: dial tcp IP: getsockopt: connection refused`, +the LFS client is trying to reach GitLab through HTTPS. However, your GitLab instance is being served on HTTP. + +This behaviour is caused by Git LFS using HTTPS connections by default when a `lfsurl` is not set in the Git config. + +To prevent this from happening, set the lfs url in project Git config: + +```bash + +git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs/objects/batch" +``` + +### Credentials are always required when pushing an object + +Given that Git LFS uses HTTP Basic Authentication to authenticate the user pushing the LFS object on every push for every object, user HTTPS credentials are required. + +By default, Git has support for remembering the credentials for each repository you use. This is described in [Git credentials man pages](https://git-scm.com/docs/gitcredentials). + +For example, you can tell Git to remember the password for a period of time in which you expect to push the objects: + +```bash +git config --global credential.helper 'cache --timeout=3600' +``` + +This will remember the credentials for an hour after which Git operations will require re-authentication. + +If you are using OS X you can use `osxkeychain` to store and encrypt your credentials. For Windows, `wincred` is available. + +More details about various methods of storing the user credentials can be found on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage). diff --git a/doc/workflow/voting_slider.png b/doc/workflow/voting_slider.png deleted file mode 100644 index 4c660ef95937c66924a2f01373cc4efed4eae831..0000000000000000000000000000000000000000 Binary files a/doc/workflow/voting_slider.png and /dev/null differ diff --git a/features/groups.feature b/features/groups.feature index 615eff6a330607aa719bc70e6ee6d7e977572b57..abf3769a84446c7355cf924a30abe0b2549892a2 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -59,6 +59,14 @@ Feature: Groups When I select "Mike" as "Reporter" Then I should see "Mike" in team list as "Reporter" + @javascript + Scenario: Ignore add user to group when is already Owner + Given gitlab user "Mike" + When I visit group "Owned" members page + And I click link "Add members" + When I select "Mike" as "Reporter" + Then I should see "Mike" in team list as "Owner" + @javascript Scenario: Invite user to group When I visit group "Owned" members page diff --git a/features/project/commits/diff_comments.feature b/features/project/commits/diff_comments.feature index 4a2b870e0822987798d4c8b238d6122a5b9a1142..d6e0c84537e89b13160c5479d491d32c57666fda 100644 --- a/features/project/commits/diff_comments.feature +++ b/features/project/commits/diff_comments.feature @@ -13,6 +13,12 @@ Feature: Project Commits Diff Comments Given I leave a diff comment like "Typo, please fix" Then I should see a diff comment saying "Typo, please fix" + @javascript + Scenario: I can add a diff comment with a single emoji + Given I open a diff comment form + And I write a diff comment like ":smile:" + Then I should see a diff comment with an emoji image + @javascript Scenario: I get a temporary form for the first comment on a diff line Given I open a diff comment form diff --git a/features/project/issues/award_emoji.feature b/features/project/issues/award_emoji.feature new file mode 100644 index 0000000000000000000000000000000000000000..2609f129d0780706fa5f234e87c8be7fe6e97835 --- /dev/null +++ b/features/project/issues/award_emoji.feature @@ -0,0 +1,18 @@ +Feature: Award Emoji + Background: + Given I sign in as a user + And I own project "Shop" + And project "Shop" has issue "Bugfix" + And I visit "Bugfix" issue page + + @javascript + Scenario: I add and remove award in the issue + Given I click to emoji-picker + And I click to emoji in the picker + Then I have award added + And I can remove it by clicking to icon + + @javascript + Scenario: I add award emoji using regular comment + Given I leave comment with a single emoji + Then I have award added diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index 69aa79f2d2462fae3a494ba539f14aa0601e88d5..e545ea63ca880efe84b78f94826df221ad421702 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -42,7 +42,7 @@ Feature: Project Source Browse Files And I fill the new branch name And I click on "Upload file" Then I can see the new text file - And I am redirected to the uploaded file on new branch + And I am redirected to the new merge request page And I can see the new commit message @javascript @@ -64,7 +64,7 @@ Feature: Project Source Browse Files And I fill the commit message And I fill the new branch name And I click on "Commit Changes" - Then I am redirected to the new file on new branch + Then I am redirected to the new merge request page And I should see its new content @javascript @@ -134,7 +134,7 @@ Feature: Project Source Browse Files And I fill the commit message And I fill the new branch name And I click on "Commit Changes" - Then I am redirected to the ".gitignore" on new branch + Then I am redirected to the new merge request page And I should see its new content @javascript @wip @@ -154,7 +154,7 @@ Feature: Project Source Browse Files And I fill the commit message And I fill the new branch name And I click on "Create directory" - Then I am redirected to the new directory + Then I am redirected to the new merge request page @javascript Scenario: I attempt to create an existing directory @@ -174,12 +174,12 @@ Feature: Project Source Browse Files Then I see diff @javascript - Scenario: I can remove file and commit + Scenario: I can delete file and commit Given I click on ".gitignore" file in repo And I see the ".gitignore" - And I click on "Remove" + And I click on "Delete" And I fill the commit message - And I click on "Remove file" + And I click on "Delete file" Then I am redirected to the files URL And I don't see the ".gitignore" diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb index 44a4aa9844a239384b943f2f21db51d39f40b3ef..a0aad66184d1900325889ad19de831a308009eea 100644 --- a/features/steps/dashboard/new_project.rb +++ b/features/steps/dashboard/new_project.rb @@ -44,7 +44,6 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps git_import_instructions = first('.js-toggle-content') expect(git_import_instructions).to be_visible expect(git_import_instructions).to have_content "Git repository URL" - expect(git_import_instructions).to have_content "The repository must be accessible over HTTP(S). If it is not publicly accessible, you can add authentication information to the URL:" end step 'I click on "Google Code"' do diff --git a/features/steps/groups.rb b/features/steps/groups.rb index a8fba2406ae0d0cd8ce54fcc2304d836498eaf4f..9c0313537b12db5573d86d1e956494dfc34e543c 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -48,6 +48,17 @@ class Spinach::Features::Groups < Spinach::FeatureSteps click_button "Add users to group" end + step 'I select "Mike" as "Master"' do + user = User.find_by(name: "Mike") + + page.within ".users-group-form" do + select2(user.id, from: "#user_ids", multiple: true) + select "Master", from: "access_level" + end + + click_button "Add users to group" + end + step 'I should see "Mike" in team list as "Reporter"' do page.within '.well-list' do expect(page).to have_content('Mike') @@ -55,6 +66,13 @@ class Spinach::Features::Groups < Spinach::FeatureSteps end end + step 'I should see "Mike" in team list as "Owner"' do + page.within '.well-list' do + expect(page).to have_content('Mike') + expect(page).to have_content('Owner') + end + end + step 'I select "sjobs@apple.com" as "Reporter"' do page.within ".users-group-form" do select2("sjobs@apple.com", from: "#user_ids", multiple: true) diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb new file mode 100644 index 0000000000000000000000000000000000000000..325eaf2ea6a5834ea388eed6e1244e152b372a88 --- /dev/null +++ b/features/steps/project/issues/award_emoji.rb @@ -0,0 +1,48 @@ +class Spinach::Features::AwardEmoji < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedPaths + include Select2Helper + + step 'I visit "Bugfix" issue page' do + visit namespace_project_issue_path(@project.namespace, @project, @issue) + end + + step 'I click to emoji-picker' do + page.within '.awards-controls' do + page.find('.add-award').click + end + end + + step 'I click to emoji in the picker' do + page.within '.awards-menu' do + page.first('img').click + end + end + + step 'I can remove it by clicking to icon' do + page.within '.awards' do + page.first('.award').click + expect(page).to_not have_selector '.award' + end + end + + step 'I have award added' do + page.within '.awards' do + expect(page).to have_selector '.award' + expect(page.find('.award .counter')).to have_content '1' + end + end + + step 'project "Shop" has issue "Bugfix"' do + @project = Project.find_by(name: 'Shop') + @issue = create(:issue, title: 'Bugfix', project: project) + end + + step 'I leave comment with a single emoji' do + page.within('.js-main-target-form') do + fill_in 'note[note]', with: ':smile:' + click_button 'Add Comment' + end + end +end diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 84725b9b585444759177ae7383729c808f5f9c23..f40e0f0d5282c3f7bf13d9f34e9d4b50c2eec03b 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -98,12 +98,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps click_button 'Create directory' end - step 'I click on "Remove"' do - click_button 'Remove' + step 'I click on "Delete"' do + click_button 'Delete' end - step 'I click on "Remove file"' do - click_button 'Remove file' + step 'I click on "Delete file"' do + click_button 'Delete file' end step 'I click on "Replace"' do @@ -142,7 +142,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I can see new file page' do - expect(page).to have_content "new file" + expect(page).to have_content "Create New File" expect(page).to have_content "Commit message" end @@ -225,10 +225,6 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps expect(current_path).to eq(namespace_project_blob_path(@project.namespace, @project, 'master/.gitignore')) end - step 'I am redirected to the ".gitignore" on new branch' do - expect(current_path).to eq(namespace_project_blob_path(@project.namespace, @project, 'new_branch_name/.gitignore')) - end - step 'I am redirected to the permalink URL' do expect(current_path).to( eq(namespace_project_blob_path(@project.namespace, @project, @@ -247,20 +243,8 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps @project.namespace, @project, 'master/' + new_file_name_with_directory)) end - step 'I am redirected to the new file on new branch' do - expect(current_path).to eq(namespace_project_blob_path( - @project.namespace, @project, 'new_branch_name/' + new_file_name)) - end - - step 'I am redirected to the uploaded file on new branch' do - expect(current_path).to eq(namespace_project_blob_path( - @project.namespace, @project, - 'new_branch_name/' + File.basename(test_text_file))) - end - - step 'I am redirected to the new directory' do - expect(current_path).to eq(namespace_project_tree_path( - @project.namespace, @project, 'new_branch_name/' + new_dir_name)) + step 'I am redirected to the new merge request page' do + expect(current_path).to eq(new_namespace_project_merge_request_path(@project.namespace, @project)) end step 'I am redirected to the root directory' do diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 72621911a37151cd82b412d46d78465b3abb7380..dd466cde28d0efb5fa6a2c0bfab6cdea68053fee 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -87,6 +87,17 @@ module SharedDiffNote end end + step 'I write a diff comment like ":smile:"' do + page.within(diff_file_selector) do + click_diff_line(sample_commit.line_code) + + page.within("form[rel$='#{sample_commit.line_code}']") do + fill_in 'note[note]', with: ':smile:' + click_button('Add Comment') + end + end + end + step 'I submit the diff comment' do page.within(diff_file_selector) do click_button("Add Comment") @@ -197,6 +208,12 @@ module SharedDiffNote end end + step 'I should see a diff comment with an emoji image' do + page.within("#{diff_file_selector} .note") do + expect(page).to have_xpath("//img[@alt=':smile:']") + end + end + step 'I click side-by-side diff button' do find('#parallel-diff-btn').trigger('click') end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index d6aec03d7f557fab6d3539388ca9db17b61529c6..9f337bc3cc6692e1cc3bd6e35f1827c11fae0162 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -162,7 +162,9 @@ module API end class MergeRequest < ProjectEntity - expose :target_branch, :source_branch, :upvotes, :downvotes + expose :target_branch, :source_branch + # deprecated, always returns 0 + expose :upvotes, :downvotes expose :author, :assignee, using: Entities::UserBasic expose :source_project_id, :target_project_id expose :label_names, as: :labels @@ -192,6 +194,7 @@ module API expose :author, using: Entities::UserBasic expose :created_at expose :system?, as: :system + # upvote? and downvote? are deprecated, always return false expose :upvote?, as: :upvote expose :downvote?, as: :downvote end @@ -324,7 +327,8 @@ module API end class Release < Grape::Entity - expose :tag, :description + expose :tag, as: :tag_name + expose :description end class RepoTag < Grape::Entity diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index 882d1a083ad828d2ea6681b0ca2e112bc636aac4..4b7e4a6b1e31d120814cc44b799a187ede963f32 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -101,10 +101,10 @@ module API required_attributes! [:hook_id] begin - @hook = ProjectHook.find(params[:hook_id]) - @hook.destroy + @hook = user_project.hooks.destroy(params[:hook_id]) rescue # ProjectHook can raise Error if hook_id not found + not_found!("Error deleting hook #{params[:hook_id]}") end 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/api/tags.rb b/lib/api/tags.rb index 673342dd44714b7d6898b74dd983d55ed309c8d1..47621f443e6c088ffd66ff804c497353314d8a28 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -44,17 +44,42 @@ module API # # Parameters: # id (required) - The ID of a project - # tag (required) - The name of the tag + # tag_name (required) - The name of the tag + # description (required) - Release notes with markdown support + # Example Request: + # POST /projects/:id/repository/tags/:tag_name/release + post ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.*/ } do + authorize_push_project + required_attributes! [:description] + result = CreateReleaseService.new(user_project, current_user). + execute(params[:tag_name], params[:description]) + + if result[:status] == :success + present result[:release], with: Entities::Release + else + render_api_error!(result[:message], result[:http_status]) + end + end + + # Updates a release notes of a tag + # + # Parameters: + # id (required) - The ID of a project + # tag_name (required) - The name of the tag # description (required) - Release notes with markdown support # Example Request: - # PUT /projects/:id/repository/tags - put ':id/repository/:tag/release', requirements: { tag: /.*/ } do + # PUT /projects/:id/repository/tags/:tag_name/release + put ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.*/ } do authorize_push_project required_attributes! [:description] - release = user_project.releases.find_or_initialize_by(tag: params[:tag]) - release.update_attributes(description: params[:description]) + result = UpdateReleaseService.new(user_project, current_user). + execute(params[:tag_name], params[:description]) - present release, with: Entities::Release + if result[:status] == :success + present result[:release], with: Entities::Release + else + render_api_error!(result[:message], result[:http_status]) + end end end end diff --git a/lib/award_emoji.rb b/lib/award_emoji.rb new file mode 100644 index 0000000000000000000000000000000000000000..d58a196c4efda38b8875ea78256072b0818a2a97 --- /dev/null +++ b/lib/award_emoji.rb @@ -0,0 +1,12 @@ +class AwardEmoji + EMOJI_LIST = [ + "+1", "-1", "100", "blush", "heart", "smile", "rage", + "beers", "disappointed", "ok_hand", + "helicopter", "shit", "airplane", "alarm_clock", + "ambulance", "anguished", "two_hearts", "wink" + ] + + def self.path_to_emoji_image(name) + "emoji/#{Emoji.emoji_filename(name)}.png" + end +end diff --git a/lib/backup/lfs.rb b/lib/backup/lfs.rb new file mode 100644 index 0000000000000000000000000000000000000000..4153467fbeeea29b981762f667939aa0440ed5b2 --- /dev/null +++ b/lib/backup/lfs.rb @@ -0,0 +1,13 @@ +require 'backup/files' + +module Backup + class Lfs < Files + def initialize + super('lfs', Settings.lfs.storage_path) + end + + def create_files_dir + Dir.mkdir(app_files_dir, 0700) + end + end +end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 9e15d5411a1742760434937936ddd2fa1621c99b..099062eeb8b885c5057aba520d575b8789c62995 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -150,17 +150,15 @@ module Backup private def backup_contents - folders_to_backup + ["uploads.tar.gz", "builds.tar.gz", "artifacts.tar.gz", "backup_information.yml"] + folders_to_backup + archives_to_backup + ["backup_information.yml"] end - def folders_to_backup - folders = %w{repositories db} - - if ENV["SKIP"] - return folders.reject{ |folder| ENV["SKIP"].include?(folder) } - end + def archives_to_backup + %w{uploads builds artifacts lfs}.map{ |name| (name + ".tar.gz") unless skipped?(name) }.compact + end - folders + def folders_to_backup + %w{repositories db}.reject{ |name| skipped?(name) } end def settings diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb index 0a58667280745de66b4fc6f89488d55fa796516d..15faa6edd8402c2ca4a11a8c7493db453153f1b3 100644 --- a/lib/ci/api/builds.rb +++ b/lib/ci/api/builds.rb @@ -58,6 +58,7 @@ module Ci # POST /builds/:id/artifacts/authorize post ":id/artifacts/authorize" do require_gitlab_workhorse! + not_allowed! unless Gitlab.config.artifacts.enabled build = Ci::Build.find_by_id(params[:id]) not_found! unless build authenticate_build_token!(build) @@ -91,6 +92,7 @@ module Ci # POST /builds/:id/artifacts post ":id/artifacts" do require_gitlab_workhorse! + not_allowed! unless Gitlab.config.artifacts.enabled build = Ci::Build.find_by_id(params[:id]) not_found! unless build authenticate_build_token!(build) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 3beafcad117f19cbaafa13f432c632d1b65ed894..e7fb1b79b7ca0c2395d55838cb63675923faae58 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -10,7 +10,7 @@ module Ci attr_reader :before_script, :image, :services, :variables, :path, :cache def initialize(config, path = nil) - @config = YAML.load(config) + @config = YAML.safe_load(config, [Symbol]) @path = path unless @config.is_a? Hash diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb index 01b8bda05c6741cf7b914735722447388cec42f7..87ac30b5ffef959e7baa2a60f8e28d41310099ed 100644 --- a/lib/gitlab/backend/shell.rb +++ b/lib/gitlab/backend/shell.rb @@ -1,6 +1,6 @@ module Gitlab class Shell - class AccessDenied < StandardError; end + class Error < StandardError; end class KeyAdder < Struct.new(:io) def add_key(id, key) @@ -36,8 +36,9 @@ module Gitlab # import_repository("gitlab/gitlab-ci", "https://github.com/randx/six.git") # def import_repository(name, url) - Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'import-project', - "#{name}.git", url, '240']) + output, status = Popen::popen([gitlab_shell_projects_path, 'import-project', "#{name}.git", url, '240']) + raise Error, output unless status.zero? + true end # Move repository diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 2d3e32d95396111ba51b887bf49c504090f5d056..46a4ef0e31febfd300cbc35bc5abb88291e59f27 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -25,7 +25,7 @@ module Gitlab session_expire_delay: Settings.gitlab['session_expire_delay'], import_sources: Settings.gitlab['import_sources'], shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], - max_artifacts_size: Ci::Settings.gitlab_ci['max_artifacts_size'], + max_artifacts_size: Settings.artifacts['max_size'], ) end diff --git a/lib/gitlab/git/hook.rb b/lib/gitlab/git/hook.rb index dd393fe09d201e8739e3fc5224061565fb36a844..07b856ca64cffec10fa8922eacf8a337bdca17d7 100644 --- a/lib/gitlab/git/hook.rb +++ b/lib/gitlab/git/hook.rb @@ -16,6 +16,17 @@ module Gitlab def trigger(gl_id, oldrev, newrev, ref) return true unless exists? + case name + when "pre-receive", "post-receive" + call_receive_hook(gl_id, oldrev, newrev, ref) + when "update" + call_update_hook(gl_id, oldrev, newrev, ref) + end + end + + private + + def call_receive_hook(gl_id, oldrev, newrev, ref) changes = [oldrev, newrev, ref].join(" ") # function will return true if succesful @@ -54,6 +65,12 @@ module Gitlab exit_status end + + def call_update_hook(gl_id, oldrev, newrev, ref) + Dir.chdir(repo_path) do + system({ 'GL_ID' => gl_id }, path, ref, oldrev, newrev) + end + end end end end diff --git a/lib/gitlab/lfs/response.rb b/lib/gitlab/lfs/response.rb index 4202c786466849b66bad3ca05ea57df866eeca74..c18dfbd485dcbbf03a8cca869f9c28117ba41b07 100644 --- a/lib/gitlab/lfs/response.rb +++ b/lib/gitlab/lfs/response.rb @@ -10,23 +10,9 @@ module Gitlab @request = request end - # Return a response for a download request - # Can be a response to: - # Request from a user to get the file - # Request from gitlab-workhorse which file to serve to the user - def render_download_hypermedia_response(oid) - render_response_to_download do - if check_download_accept_header? - render_lfs_download_hypermedia(oid) - else - render_not_found - end - end - end - def render_download_object_response(oid) render_response_to_download do - if check_download_sendfile_header? && check_download_accept_header? + if check_download_sendfile_header? render_lfs_sendfile(oid) else render_not_found @@ -34,20 +20,15 @@ module Gitlab end end - def render_lfs_api_auth - render_response_to_push do - request_body = JSON.parse(@request.body.read) - return render_not_found if request_body.empty? || request_body['objects'].empty? - - response = build_response(request_body['objects']) - [ - 200, - { - "Content-Type" => "application/json; charset=utf-8", - "Cache-Control" => "private", - }, - [JSON.dump(response)] - ] + def render_batch_operation_response + request_body = JSON.parse(@request.body.read) + case request_body["operation"] + when "download" + render_batch_download(request_body) + when "upload" + render_batch_upload(request_body) + else + render_not_found end end @@ -71,13 +52,24 @@ module Gitlab end end + def render_unsupported_deprecated_api + [ + 501, + { "Content-Type" => "application/json; charset=utf-8" }, + [JSON.dump({ + 'message' => 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.', + 'documentation_url' => "#{Gitlab.config.gitlab.url}/help", + })] + ] + end + private def render_not_enabled [ 501, { - "Content-Type" => "application/vnd.git-lfs+json", + "Content-Type" => "application/json; charset=utf-8", }, [JSON.dump({ 'message' => 'Git LFS is not enabled on this GitLab server, contact your admin.', @@ -142,18 +134,35 @@ module Gitlab end end - def render_lfs_download_hypermedia(oid) - return render_not_found unless oid.present? + def render_batch_upload(body) + return render_not_found if body.empty? || body['objects'].nil? - lfs_object = object_for_download(oid) - if lfs_object + render_response_to_push do + response = build_upload_batch_response(body['objects']) [ 200, - { "Content-Type" => "application/vnd.git-lfs+json" }, - [JSON.dump(download_hypermedia(oid))] + { + "Content-Type" => "application/json; charset=utf-8", + "Cache-Control" => "private", + }, + [JSON.dump(response)] + ] + end + end + + def render_batch_download(body) + return render_not_found if body.empty? || body['objects'].nil? + + render_response_to_download do + response = build_download_batch_response(body['objects']) + [ + 200, + { + "Content-Type" => "application/json; charset=utf-8", + "Cache-Control" => "private", + }, + [JSON.dump(response)] ] - else - render_not_found end end @@ -199,10 +208,6 @@ module Gitlab @env['HTTP_X_SENDFILE_TYPE'].to_s == "X-Sendfile" end - def check_download_accept_header? - @env['HTTP_ACCEPT'].to_s == "application/vnd.git-lfs+json; charset=utf-8" - end - def user_can_fetch? # Check user access against the project they used to initiate the pull @user.can?(:download_code, @origin_project) @@ -266,42 +271,56 @@ module Gitlab @project.lfs_objects.where(oid: objects_oids).pluck(:oid).to_set end - def build_response(objects) + def build_upload_batch_response(objects) selected_objects = select_existing_objects(objects) - upload_hypermedia(objects, selected_objects) + upload_hypermedia_links(objects, selected_objects) end - def download_hypermedia(oid) - { - '_links' => { - 'download' => - { - 'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{oid}", - 'header' => { - 'Accept' => "application/vnd.git-lfs+json; charset=utf-8", - 'Authorization' => @env['HTTP_AUTHORIZATION'] - }.compact - } - } - } + def build_download_batch_response(objects) + selected_objects = select_existing_objects(objects) + + download_hypermedia_links(objects, selected_objects) end - def upload_hypermedia(all_objects, existing_objects) + def download_hypermedia_links(all_objects, existing_objects) all_objects.each do |object| - object['_links'] = hypermedia_links(object) unless existing_objects.include?(object['oid']) + if existing_objects.include?(object['oid']) + object['actions'] = { + 'download' => { + 'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}", + 'header' => { + 'Authorization' => @env['HTTP_AUTHORIZATION'] + }.compact + } + } + else + object['error'] = { + 'code' => 404, + 'message' => "Object does not exist on the server or you don't have permissions to access it", + } + end end { 'objects' => all_objects } end - def hypermedia_links(object) - { - "upload" => { - 'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}/#{object['size']}", - 'header' => { 'Authorization' => @env['HTTP_AUTHORIZATION'] } - }.compact - } + def upload_hypermedia_links(all_objects, existing_objects) + all_objects.each do |object| + # generate actions only for non-existing objects + next if existing_objects.include?(object['oid']) + + object['actions'] = { + 'upload' => { + 'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}/#{object['size']}", + 'header' => { + 'Authorization' => @env['HTTP_AUTHORIZATION'] + }.compact + } + } + end + + { 'objects' => all_objects } end end end diff --git a/lib/gitlab/lfs/router.rb b/lib/gitlab/lfs/router.rb index 4809e8349847398f6ca13ceaf3ccaf72f7c6d24d..78d028911021612dc90709302f46e02ca535a9d7 100644 --- a/lib/gitlab/lfs/router.rb +++ b/lib/gitlab/lfs/router.rb @@ -34,7 +34,7 @@ module Gitlab case path_match[1] when "info/lfs" - lfs.render_download_hypermedia_response(oid) + lfs.render_unsupported_deprecated_api when "gitlab-lfs" lfs.render_download_object_response(oid) else @@ -48,7 +48,9 @@ module Gitlab # Check for Batch API if post_path[0].ends_with?("/info/lfs/objects/batch") - lfs.render_lfs_api_auth + lfs.render_batch_operation_response + elsif post_path[0].ends_with?("/info/lfs/objects") + lfs.render_unsupported_deprecated_api else nil end diff --git a/lib/gitlab/markdown/external_link_filter.rb b/lib/gitlab/markdown/external_link_filter.rb index 29e51b6ade672d056230e683490d3d4ea95d6dec..30171d93a4b69b8fb7523e2f17cc56b606b587e6 100644 --- a/lib/gitlab/markdown/external_link_filter.rb +++ b/lib/gitlab/markdown/external_link_filter.rb @@ -3,8 +3,7 @@ require 'html/pipeline/filter' module Gitlab module Markdown - # HTML Filter to add a `rel="nofollow"` attribute to external links - # + # HTML Filter to modify the attributes of external links class ExternalLinkFilter < HTML::Pipeline::Filter def call doc.search('a').each do |node| @@ -18,7 +17,7 @@ module Gitlab # Skip internal links next if link.start_with?(internal_url) - node.set_attribute('rel', 'nofollow') + node.set_attribute('rel', 'nofollow noreferrer') end doc diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb index fa068d507634a1351985bab5e2877dfd8484e1e3..4f9cdef3869e5b53743467a97e83c0ef07174bb2 100644 --- a/lib/gitlab/push_data_builder.rb +++ b/lib/gitlab/push_data_builder.rb @@ -18,10 +18,7 @@ module Gitlab # homepage: String, # }, # commits: Array, - # total_commits_count: Fixnum, - # added: ["CHANGELOG"], - # modified: [], - # removed: ["tmp/file.txt"] + # total_commits_count: Fixnum # } # def build(project, user, oldrev, newrev, ref, commits = [], message = nil) @@ -33,11 +30,12 @@ module Gitlab # For performance purposes maximum 20 latest commits # will be passed as post receive hook data. - commit_attrs = commits_limited.map(&:hook_attrs) + commit_attrs = commits_limited.map do |commit| + commit.hook_attrs(with_changed_files: true) + end type = Gitlab::Git.tag_ref?(ref) ? "tag_push" : "push" - repo_changes = repo_changes(project, newrev, oldrev) # Hash to be passed as post_receive_data data = { object_kind: type, @@ -60,10 +58,7 @@ module Gitlab visibility_level: project.visibility_level }, commits: commit_attrs, - total_commits_count: commits_count, - added: repo_changes[:added], - modified: repo_changes[:modified], - removed: repo_changes[:removed] + total_commits_count: commits_count } data @@ -94,27 +89,6 @@ module Gitlab newrev end end - - def repo_changes(project, newrev, oldrev) - changes = { added: [], modified: [], removed: [] } - compare_result = CompareService.new. - execute(project, newrev, project, oldrev) - - if compare_result - compare_result.diffs.each do |diff| - case true - when diff.deleted_file - changes[:removed] << diff.old_path - when diff.renamed_file, diff.new_file - changes[:added] << diff.new_path - else - changes[:modified] << diff.new_path - end - end - end - - changes - end end end end diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb new file mode 100644 index 0000000000000000000000000000000000000000..1cd89b3a9c40b5de369023805e0942c014be5e3d --- /dev/null +++ b/lib/gitlab/sql/union.rb @@ -0,0 +1,34 @@ +module Gitlab + module SQL + # Class for building SQL UNION statements. + # + # ORDER BYs are dropped from the relations as the final sort order is not + # guaranteed any way. + # + # Example usage: + # + # union = Gitlab::SQL::Union.new(user.personal_projects, user.projects) + # sql = union.to_sql + # + # Project.where("id IN (#{sql})") + class Union + def initialize(relations) + @relations = relations + end + + def to_sql + # Some relations may include placeholders for prepared statements, these + # aren't incremented properly when joining relations together this way. + # By using "unprepared_statements" we remove the usage of placeholders + # (thus fixing this problem), at a slight performance cost. + fragments = ActiveRecord::Base.connection.unprepared_statement do + @relations.map do |rel| + rel.reorder(nil).to_sql + end + end + + fragments.join("\nUNION\n") + end + end + end +end diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index 93f2ad07aeb2e5e3ddfdb0663e015a0a37cccf4b..2a79fbdcf93c8fddfd1c487318f88c09d0bbaf2e 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -67,7 +67,7 @@ server { location / { ## Serve static files from defined root folder. ## @gitlab is a named location for the upstream fallback, see below. - try_files $uri $uri/index.html $uri.html @gitlab; + try_files $uri /index.html $uri.html @gitlab; } ## We route uploads through GitLab to prevent XSS and enforce access control. @@ -121,18 +121,21 @@ server { } location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ { + client_max_body_size 0; # 'Error' 418 is a hack to re-use the @gitlab-workhorse block error_page 418 = @gitlab-workhorse; return 418; } location ~ ^/[\w\.-]+/[\w\.-]+/repository/archive { + client_max_body_size 0; # 'Error' 418 is a hack to re-use the @gitlab-workhorse block error_page 418 = @gitlab-workhorse; return 418; } location ~ ^/api/v3/projects/.*/repository/archive { + client_max_body_size 0; # 'Error' 418 is a hack to re-use the @gitlab-workhorse block error_page 418 = @gitlab-workhorse; return 418; @@ -140,21 +143,22 @@ server { # Build artifacts should be submitted to this location location ~ ^/[\w\.-]+/[\w\.-]+/builds/download { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; + client_max_body_size 0; + # 'Error' 418 is a hack to re-use the @gitlab-workhorse block + error_page 418 = @gitlab-workhorse; + return 418; } # Build artifacts should be submitted to this location location ~ /ci/api/v1/builds/[0-9]+/artifacts { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; + client_max_body_size 0; + # 'Error' 418 is a hack to re-use the @gitlab-workhorse block + error_page 418 = @gitlab-workhorse; + return 418; } location @gitlab-workhorse { + client_max_body_size 0; ## If you use HTTPS make sure you disable gzip compression ## to be safe against BREACH attack. # gzip off; diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 90749947fa4f08ebcbee58d1fd9c53b0f5cb6a7b..016f7a536fb854e38adc1fbc9083e8e7f93f0c9d 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -112,7 +112,7 @@ server { location / { ## Serve static files from defined root folder. ## @gitlab is a named location for the upstream fallback, see below. - try_files $uri $uri/index.html $uri.html @gitlab; + try_files $uri /index.html $uri.html @gitlab; } ## We route uploads through GitLab to prevent XSS and enforce access control. @@ -168,18 +168,21 @@ server { } location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ { + client_max_body_size 0; # 'Error' 418 is a hack to re-use the @gitlab-workhorse block error_page 418 = @gitlab-workhorse; return 418; } location ~ ^/[\w\.-]+/[\w\.-]+/repository/archive { + client_max_body_size 0; # 'Error' 418 is a hack to re-use the @gitlab-workhorse block error_page 418 = @gitlab-workhorse; return 418; } location ~ ^/api/v3/projects/.*/repository/archive { + client_max_body_size 0; # 'Error' 418 is a hack to re-use the @gitlab-workhorse block error_page 418 = @gitlab-workhorse; return 418; @@ -187,21 +190,22 @@ server { # Build artifacts should be submitted to this location location ~ ^/[\w\.-]+/[\w\.-]+/builds/download { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; + client_max_body_size 0; + # 'Error' 418 is a hack to re-use the @gitlab-workhorse block + error_page 418 = @gitlab-workhorse; + return 418; } # Build artifacts should be submitted to this location location ~ /ci/api/v1/builds/[0-9]+/artifacts { - client_max_body_size 0; - # 'Error' 418 is a hack to re-use the @gitlab-workhorse block - error_page 418 = @gitlab-workhorse; - return 418; + client_max_body_size 0; + # 'Error' 418 is a hack to re-use the @gitlab-workhorse block + error_page 418 = @gitlab-workhorse; + return 418; } location @gitlab-workhorse { + client_max_body_size 0; ## If you use HTTPS make sure you disable gzip compression ## to be safe against BREACH attack. gzip off; diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index 3c46bcea40edd172754113d792537820d8f2ce06..cb4abe13799f426cac93a62a95a3d770626ec665 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -13,6 +13,7 @@ namespace :gitlab do Rake::Task["gitlab:backup:uploads:create"].invoke Rake::Task["gitlab:backup:builds:create"].invoke Rake::Task["gitlab:backup:artifacts:create"].invoke + Rake::Task["gitlab:backup:lfs:create"].invoke backup = Backup::Manager.new backup.pack @@ -34,6 +35,7 @@ namespace :gitlab do Rake::Task["gitlab:backup:uploads:restore"].invoke unless backup.skipped?("uploads") Rake::Task["gitlab:backup:builds:restore"].invoke unless backup.skipped?("builds") Rake::Task["gitlab:backup:artifacts:restore"].invoke unless backup.skipped?("artifacts") + Rake::Task["gitlab:backup:lfs:restore"].invoke unless backup.skipped?("lfs") Rake::Task["gitlab:shell:setup"].invoke backup.cleanup @@ -134,6 +136,25 @@ namespace :gitlab do end end + namespace :lfs do + task create: :environment do + $progress.puts "Dumping lfs objects ... ".blue + + if ENV["SKIP"] && ENV["SKIP"].include?("lfs") + $progress.puts "[SKIPPED]".cyan + else + Backup::Lfs.new.dump + $progress.puts "done".green + end + end + + task restore: :environment do + $progress.puts "Restoring lfs objects ... ".blue + Backup::Lfs.new.restore + $progress.puts "done".green + end + end + def configure_cron_mode if ENV['CRON'] # We need an object we can say 'puts' and 'print' to; let's use a diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index dca5e1c5db3b141d7e0f34c4dd0fbc48f2f7e452..247383aa46c6423486d9f864b6908a24129b27e1 100755 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -1,10 +1,30 @@ #!/bin/bash -if [ -f /.dockerinit ]; then - wget -q http://ftp.de.debian.org/debian/pool/main/p/phantomjs/phantomjs_1.9.0-1+b1_amd64.deb - dpkg -i phantomjs_1.9.0-1+b1_amd64.deb - apt-get update -qq - apt-get install -y -qq libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client +retry() { + for i in $(seq 1 3); do + if eval "$@"; then + return 0 + fi + sleep 3s + echo "Retrying..." + done + return 1 +} + +if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then + mkdir -p vendor + + # Install phantomjs package + pushd vendor + if [ ! -e phantomjs_1.9.8-0jessie_amd64.deb ]; then + wget -q https://gitlab.com/axil/phantomjs-debian/raw/master/phantomjs_1.9.8-0jessie_amd64.deb + fi + dpkg -i phantomjs_1.9.8-0jessie_amd64.deb + popd + + # Try to install packages + retry 'apt-get update -yqqq; apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \ + libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip' cp config/database.yml.mysql config/database.yml sed -i 's/username:.*/username: root/g' config/database.yml @@ -13,8 +33,8 @@ if [ -f /.dockerinit ]; then cp config/resque.yml.example config/resque.yml sed -i 's/localhost/redis/g' config/resque.yml - FLAGS=(--deployment --path /cache) - export FLAGS + + export FLAGS=(--path vendor --retry 3) else export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin cp config/database.yml.mysql config/database.yml 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 8b7af4d3a0aa94542ab494ca663693142ac4b409..26e35929a6ca5cfb0a3d9dee2c40dfc01b6ab81a 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' describe Admin::UsersController do - let(:admin) { create(:admin) } + let(:user) { create(:user) } + let(:admin) { create(:admin) } before do sign_in(admin) @@ -104,4 +105,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/commit_controller_spec.rb b/spec/controllers/commit_controller_spec.rb index bb3d87f384081f488d4fd500e837be9a72d75661..ab7c7a24065e64bdb915bdf62a5c6a07c002cce7 100644 --- a/spec/controllers/commit_controller_spec.rb +++ b/spec/controllers/commit_controller_spec.rb @@ -95,6 +95,26 @@ describe Projects::CommitController do expect(response.body).to match(/^diff --git/) end end + + context 'commit that removes a submodule' do + render_views + + let(:fork_project) { create(:forked_project_with_submodules) } + let(:commit) { fork_project.commit('remove-submodule') } + + before do + fork_project.team << [user, :master] + end + + it 'renders it' do + get(:show, + namespace_id: fork_project.namespace.to_param, + project_id: fork_project.to_param, + id: commit.id) + + expect(response).to be_success + end + end end describe "#branches" do diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..eb0c6ac6d806b1d9d3d0d83a32794a98001991ed --- /dev/null +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Groups::MilestonesController do + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + let(:project2) { create(:empty_project, group: group) } + let(:user) { create(:user) } + let(:title) { '肯定不是中文的问题' } + + before do + sign_in(user) + group.add_owner(user) + project.team << [user, :master] + controller.instance_variable_set(:@group, group) + end + + describe "#create" do + it "should create group milestone with Chinese title" do + post :create, + group_id: group.id, + milestone: { project_ids: [project.id, project2.id], title: title } + + expect(response).to redirect_to(group_milestone_path(group, title.to_slug.to_s, title: title)) + expect(Milestone.where(title: title).count).to eq(2) + end + end +end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 3e5e1fa87ae710b80cbddc2c40d63e7e8ecb56af..6aaec224f6eeff04ceaed2106978fa40201186ab 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -10,6 +10,30 @@ describe Projects::MergeRequestsController do project.team << [user, :master] end + describe '#new' do + context 'merge request that removes a submodule' do + render_views + + let(:fork_project) { create(:forked_project_with_submodules) } + + before do + fork_project.team << [user, :master] + end + + it 'renders it' do + get :new, + namespace_id: fork_project.namespace.to_param, + project_id: fork_project.to_param, + merge_request: { + source_branch: 'remove-submodule', + target_branch: 'master' + } + + expect(response).to be_success + end + end + end + describe "#show" do shared_examples "export merge as" do |format| it "should generally work" do diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb index 8127efabe6e3e15e24ade7d24ec578f1f4b1eeda..d173bb350f1c66c0615d3114ffa5a4290e37ce87 100644 --- a/spec/controllers/projects/milestones_controller_spec.rb +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -5,7 +5,7 @@ describe Projects::MilestonesController do let(:user) { create(:user) } let(:milestone) { create(:milestone, project: project) } let(:issue) { create(:issue, project: project, milestone: milestone) } - let(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) } + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) } before do sign_in(user) @@ -15,10 +15,9 @@ describe Projects::MilestonesController do describe "#destroy" do it "should remove milestone" do - merge_request.reload expect(issue.milestone_id).to eq(milestone.id) - delete :destroy, namespace_id: project.namespace.id, project_id: project.id, id: milestone.id, format: :js + delete :destroy, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid, format: :js expect(response).to be_success expect(Event.first.action).to eq(Event::DESTROYED) diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 4bb47c6b02559c73c815f659801c16060aa13b80..665526fde9312c5f16ef0aab56f1dedeeb1552e5 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -88,6 +88,22 @@ describe ProjectsController do end end + describe "#destroy" do + let(:admin) { create(:admin) } + + it "redirects to the dashboard" do + controller.instance_variable_set(:@project, project) + sign_in(admin) + + orig_id = project.id + delete :destroy, namespace_id: project.namespace.path, id: project.path + + expect { Project.find(orig_id) }.to raise_error(ActiveRecord::RecordNotFound) + expect(response.status).to eq(302) + expect(response).to redirect_to(dashboard_projects_path) + end + end + describe "POST #toggle_star" do it "toggles star if user is signed in" do sign_in(user) diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..b3dcb52c5003bcd79bc187805f0766c7e7ca4992 --- /dev/null +++ b/spec/controllers/snippets_controller_spec.rb @@ -0,0 +1,233 @@ +require 'spec_helper' + +describe SnippetsController do + describe 'GET #show' do + let(:user) { create(:user) } + + context 'when the personal snippet is private' do + let(:personal_snippet) { create(:personal_snippet, :private, author: user) } + + context 'when signed in' do + before do + sign_in(user) + end + + context 'when signed in user is not the author' do + let(:other_author) { create(:author) } + let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) } + + it 'responds with status 404' do + get :show, id: other_personal_snippet.to_param + + expect(response.status).to eq(404) + end + end + + context 'when signed in user is the author' do + it 'renders the snippet' do + get :show, id: personal_snippet.to_param + + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response.status).to eq(200) + end + end + end + + context 'when not signed in' do + it 'redirects to the sign in page' do + get :show, id: personal_snippet.to_param + + expect(response).to redirect_to(new_user_session_path) + end + end + end + + context 'when the personal snippet is internal' do + let(:personal_snippet) { create(:personal_snippet, :internal, author: user) } + + context 'when signed in' do + before do + sign_in(user) + end + + it 'renders the snippet' do + get :show, id: personal_snippet.to_param + + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response.status).to eq(200) + end + end + + context 'when not signed in' do + it 'redirects to the sign in page' do + get :show, id: personal_snippet.to_param + + expect(response).to redirect_to(new_user_session_path) + end + end + end + + context 'when the personal snippet is public' do + let(:personal_snippet) { create(:personal_snippet, :public, author: user) } + + context 'when signed in' do + before do + sign_in(user) + end + + it 'renders the snippet' do + get :show, id: personal_snippet.to_param + + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response.status).to eq(200) + end + end + + context 'when not signed in' do + it 'renders the snippet' do + get :show, id: personal_snippet.to_param + + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response.status).to eq(200) + end + end + end + + context 'when the personal snippet does not exist' do + context 'when signed in' do + before do + sign_in(user) + end + + it 'responds with status 404' do + get :show, id: 'doesntexist' + + expect(response.status).to eq(404) + end + end + + context 'when not signed in' do + it 'responds with status 404' do + get :show, id: 'doesntexist' + + expect(response.status).to eq(404) + end + end + end + end + + describe 'GET #raw' do + let(:user) { create(:user) } + + context 'when the personal snippet is private' do + let(:personal_snippet) { create(:personal_snippet, :private, author: user) } + + context 'when signed in' do + before do + sign_in(user) + end + + context 'when signed in user is not the author' do + let(:other_author) { create(:author) } + let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) } + + it 'responds with status 404' do + get :raw, id: other_personal_snippet.to_param + + expect(response.status).to eq(404) + end + end + + context 'when signed in user is the author' do + it 'renders the raw snippet' do + get :raw, id: personal_snippet.to_param + + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response.status).to eq(200) + end + end + end + + context 'when not signed in' do + it 'redirects to the sign in page' do + get :raw, id: personal_snippet.to_param + + expect(response).to redirect_to(new_user_session_path) + end + end + end + + context 'when the personal snippet is internal' do + let(:personal_snippet) { create(:personal_snippet, :internal, author: user) } + + context 'when signed in' do + before do + sign_in(user) + end + + it 'renders the raw snippet' do + get :raw, id: personal_snippet.to_param + + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response.status).to eq(200) + end + end + + context 'when not signed in' do + it 'redirects to the sign in page' do + get :raw, id: personal_snippet.to_param + + expect(response).to redirect_to(new_user_session_path) + end + end + end + + context 'when the personal snippet is public' do + let(:personal_snippet) { create(:personal_snippet, :public, author: user) } + + context 'when signed in' do + before do + sign_in(user) + end + + it 'renders the raw snippet' do + get :raw, id: personal_snippet.to_param + + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response.status).to eq(200) + end + end + + context 'when not signed in' do + it 'renders the raw snippet' do + get :raw, id: personal_snippet.to_param + + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response.status).to eq(200) + end + end + end + + context 'when the personal snippet does not exist' do + context 'when signed in' do + before do + sign_in(user) + end + + it 'responds with status 404' do + get :raw, id: 'doesntexist' + + expect(response.status).to eq(404) + end + end + + context 'when not signed in' do + it 'responds with status 404' do + get :raw, id: 'doesntexist' + + expect(response.status).to eq(404) + end + end + end + end +end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 9f89101d7f720a56b1ab33fb36f18299c296617b..104a5f501432f7c25b520e16b91ef26ec3d4be12 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -16,13 +16,26 @@ describe UsersController do context 'with rendered views' do render_views - it 'renders the show template' do - sign_in(user) + describe 'when logged in' do + before do + sign_in(user) + end - get :show, username: user.username + it 'renders the show template' do + get :show, username: user.username - expect(response).to be_success - expect(response).to render_template('show') + expect(response).to be_success + expect(response).to render_template('show') + end + end + + describe 'when logged out' do + it 'renders the show template' do + get :show, username: user.username + + expect(response).to be_success + expect(response).to render_template('show') + end end end end diff --git a/spec/factories.rb b/spec/factories.rb index 200f18f660d52b4b32e36c7fadff1f3702c66051..4bf93adabe27c31e5977daa3eb85726ce88e2a17 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -165,6 +165,18 @@ FactoryGirl.define do title content file_name + + trait :public do + visibility_level Gitlab::VisibilityLevel::PUBLIC + end + + trait :internal do + visibility_level Gitlab::VisibilityLevel::INTERNAL + end + + trait :private do + visibility_level Gitlab::VisibilityLevel::PRIVATE + end end factory :snippet do diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index 5213ce1099fd0521bfffeac30ce81050aea5c985..435214a32ac6ea0f46b266e19991aeda1cad6212 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -7,6 +7,7 @@ describe "Builds" do login_as(:user) @commit = FactoryGirl.create :ci_commit @build = FactoryGirl.create :ci_build, commit: @commit + @build2 = FactoryGirl.create :ci_build @gl_project = @commit.project.gl_project @gl_project.team << [@user, :master] end @@ -61,13 +62,24 @@ describe "Builds" do end describe "GET /:project/builds/:id" do - before do - visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + context "Build from project" do + before do + visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + end + + it { expect(page.status_code).to eq(200) } + it { expect(page).to have_content @commit.sha[0..7] } + it { expect(page).to have_content @commit.git_commit_message } + it { expect(page).to have_content @commit.git_author_name } end - it { expect(page).to have_content @commit.sha[0..7] } - it { expect(page).to have_content @commit.git_commit_message } - it { expect(page).to have_content @commit.git_author_name } + context "Build from other project" do + before do + visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build2) + end + + it { expect(page.status_code).to eq(404) } + end context "Download artifacts" do before do @@ -80,35 +92,92 @@ describe "Builds" do end describe "POST /:project/builds/:id/cancel" do - before do - @build.run! - visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) - click_link "Cancel" + context "Build from project" do + before do + @build.run! + visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + click_link "Cancel" + end + + it { expect(page.status_code).to eq(200) } + it { expect(page).to have_content 'canceled' } + it { expect(page).to have_content 'Retry' } end - it { expect(page).to have_content 'canceled' } - it { expect(page).to have_content 'Retry' } + context "Build from other project" do + before do + @build.run! + visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + page.driver.post(cancel_namespace_project_build_path(@gl_project.namespace, @gl_project, @build2)) + end + + it { expect(page.status_code).to eq(404) } + end end describe "POST /:project/builds/:id/retry" do - before do - @build.run! - visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) - click_link "Cancel" - click_link 'Retry' + context "Build from project" do + before do + @build.run! + visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + click_link 'Cancel' + click_link 'Retry' + end + + it { expect(page.status_code).to eq(200) } + it { expect(page).to have_content 'pending' } + it { expect(page).to have_content 'Cancel' } end - it { expect(page).to have_content 'pending' } - it { expect(page).to have_content 'Cancel' } + context "Build from other project" do + before do + @build.run! + visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + click_link 'Cancel' + page.driver.post(retry_namespace_project_build_path(@gl_project.namespace, @gl_project, @build2)) + end + + it { expect(page.status_code).to eq(404) } + end end describe "GET /:project/builds/:id/download" do - before do - @build.update_attributes(artifacts_file: artifacts_file) - visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) - click_link 'Download artifacts' + context "Build from project" do + before do + @build.update_attributes(artifacts_file: artifacts_file) + visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + click_link 'Download artifacts' + end + + it { expect(page.status_code).to eq(200) } + it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) } + end + + context "Build from other project" do + before do + @build2.update_attributes(artifacts_file: artifacts_file) + visit download_namespace_project_build_path(@gl_project.namespace, @gl_project, @build2) + end + + it { expect(page.status_code).to eq(404) } + end + end + + describe "GET /:project/builds/:id/status" do + context "Build from project" do + before do + visit status_namespace_project_build_path(@gl_project.namespace, @gl_project, @build) + end + + it { expect(page.status_code).to eq(200) } end - it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) } + context "Build from other project" do + before do + visit status_namespace_project_build_path(@gl_project.namespace, @gl_project, @build2) + end + + it { expect(page.status_code).to eq(404) } + end end end diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index fdd8cf07b12cf14c792caaaac76296ac368206f1..755aca4d51eff88bc068bd020f25b7796dcb4448 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -165,7 +165,12 @@ describe 'GitLab Markdown', feature: true do describe 'ExternalLinkFilter' do it 'adds nofollow to external link' do link = doc.at_css('a:contains("Google")') - expect(link.attr('rel')).to match 'nofollow' + expect(link.attr('rel')).to include('nofollow') + end + + it 'adds noreferrer to external link' do + link = doc.at_css('a:contains("Google")') + expect(link.attr('rel')).to include('noreferrer') end it 'ignores internal link' do diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f2dd2c56d1ed2fb1ffebd3710823d273f7462bb5 --- /dev/null +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +feature 'Create New Merge Request', feature: true, js: true do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + + before do + project.team << [user, :master] + + login_as user + visit namespace_project_merge_requests_path(project.namespace, project) + 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/password_reset_spec.rb b/spec/features/password_reset_spec.rb index 85e70b4d47f3dcbf547724b6b0991c3e4447ac6b..257d363438cdf9ea4ee42f9f13be88fa2d739146 100644 --- a/spec/features/password_reset_spec.rb +++ b/spec/features/password_reset_spec.rb @@ -3,11 +3,12 @@ require 'spec_helper' feature 'Password reset', feature: true do describe 'throttling' do it 'sends reset instructions when not previously sent' do - visit root_path - forgot_password(create(:user)) + user = create(:user) + forgot_password(user) - expect(page).to have_content(I18n.t('devise.passwords.send_instructions')) + expect(page).to have_content(I18n.t('devise.passwords.send_paranoid_instructions')) expect(current_path).to eq new_user_session_path + expect(user.recently_sent_password_reset?).to be_truthy end it 'sends reset instructions when previously sent more than a minute ago' do @@ -15,26 +16,25 @@ feature 'Password reset', feature: true do user.send_reset_password_instructions user.update_attribute(:reset_password_sent_at, 5.minutes.ago) - visit root_path - forgot_password(user) - - expect(page).to have_content(I18n.t('devise.passwords.send_instructions')) + expect{ forgot_password(user) }.to change{ user.reset_password_sent_at } + expect(page).to have_content(I18n.t('devise.passwords.send_paranoid_instructions')) expect(current_path).to eq new_user_session_path end - it "throttles multiple resets in a short timespan" do + it 'throttles multiple resets in a short timespan' do user = create(:user) user.send_reset_password_instructions + # Reload because PG handles datetime less precisely than Ruby/Rails + user.reload - visit root_path - forgot_password(user) - - expect(page).to have_content(I18n.t('devise.passwords.recently_reset')) - expect(current_path).to eq new_user_password_path + expect{ forgot_password(user) }.not_to change{ user.reset_password_sent_at } + expect(page).to have_content(I18n.t('devise.passwords.send_paranoid_instructions')) + expect(current_path).to eq new_user_session_path end end def forgot_password(user) + visit root_path click_on 'Forgot your password?' fill_in 'Email', with: user.email click_button 'Reset password' diff --git a/spec/finders/contributed_projects_finder_spec.rb b/spec/finders/contributed_projects_finder_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..65d7f14c72143258e9cc634dbc8bc555e43ced49 --- /dev/null +++ b/spec/finders/contributed_projects_finder_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe ContributedProjectsFinder do + let(:source_user) { create(:user) } + let(:current_user) { create(:user) } + + let(:finder) { described_class.new(source_user) } + + let!(:public_project) { create(:project, :public) } + let!(:private_project) { create(:project, :private) } + + before do + private_project.team << [source_user, Gitlab::Access::MASTER] + private_project.team << [current_user, Gitlab::Access::DEVELOPER] + public_project.team << [source_user, Gitlab::Access::MASTER] + + create(:event, action: Event::PUSHED, project: public_project, + target: public_project, author: source_user) + + create(:event, action: Event::PUSHED, project: private_project, + target: private_project, author: source_user) + end + + describe 'without a current user' do + subject { finder.execute } + + it { is_expected.to eq([public_project]) } + end + + describe 'with a current user' do + subject { finder.execute(current_user) } + + it { is_expected.to eq([private_project, public_project]) } + end +end diff --git a/spec/finders/group_finder_spec.rb b/spec/finders/group_finder_spec.rb deleted file mode 100644 index 78dc027837cf571b23a04e8344c3c168240991a5..0000000000000000000000000000000000000000 --- a/spec/finders/group_finder_spec.rb +++ /dev/null @@ -1,15 +0,0 @@ -require 'spec_helper' - -describe GroupsFinder do - let(:user) { create :user } - let!(:group) { create :group } - let!(:public_group) { create :group, public: true } - - describe :execute do - it 'finds public group' do - groups = GroupsFinder.new.execute(user) - expect(groups.size).to eq(1) - expect(groups.first).to eq(public_group) - end - end -end diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..4f6a000822e6eae5c299262d58767763720c84be --- /dev/null +++ b/spec/finders/groups_finder_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe GroupsFinder do + describe '#execute' do + let(:user) { create(:user) } + + let(:group1) { create(:group) } + let(:group2) { create(:group) } + let(:group3) { create(:group) } + let(:group4) { create(:group, public: true) } + + let!(:public_project) { create(:project, :public, group: group1) } + let!(:internal_project) { create(:project, :internal, group: group2) } + let!(:private_project) { create(:project, :private, group: group3) } + + let(:finder) { described_class.new } + + describe 'with a user' do + subject { finder.execute(user) } + + describe 'when the user is not a member of any groups' do + it { is_expected.to eq([group4, group2, group1]) } + end + + describe 'when the user is a member of a group' do + before do + group3.add_user(user, Gitlab::Access::DEVELOPER) + end + + it { is_expected.to eq([group4, group3, group2, group1]) } + end + + describe 'when the user is a member of a private project' do + before do + private_project.team.add_user(user, Gitlab::Access::DEVELOPER) + end + + it { is_expected.to eq([group4, group3, group2, group1]) } + end + end + + describe 'without a user' do + subject { finder.execute } + + it { is_expected.to eq([group4, group1]) } + end + end +end diff --git a/spec/finders/joined_groups_finder_spec.rb b/spec/finders/joined_groups_finder_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..2d9068cc720efd8f12a78b1a3ed3243d27dc19e7 --- /dev/null +++ b/spec/finders/joined_groups_finder_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe JoinedGroupsFinder do + describe '#execute' do + let(:source_user) { create(:user) } + let(:current_user) { create(:user) } + + let(:group1) { create(:group) } + let(:group2) { create(:group) } + let(:group3) { create(:group) } + let(:group4) { create(:group, public: true) } + + let!(:public_project) { create(:project, :public, group: group1) } + let!(:internal_project) { create(:project, :internal, group: group2) } + let!(:private_project) { create(:project, :private, group: group3) } + + let(:finder) { described_class.new(source_user) } + + before do + [group1, group2, group3, group4].each do |group| + group.add_user(source_user, Gitlab::Access::MASTER) + end + end + + describe 'with a current user' do + describe 'when the current user has access to the projects of the source user' do + before do + private_project.team.add_user(current_user, Gitlab::Access::DEVELOPER) + end + + subject { finder.execute(current_user) } + + it { is_expected.to eq([group4, group3, group2, group1]) } + end + + describe 'when the current user does not have access to the projects of the source user' do + subject { finder.execute(current_user) } + + it { is_expected.to eq([group4, group2, group1]) } + end + end + + describe 'without a current user' do + subject { finder.execute } + + it { is_expected.to eq([group4, group1]) } + end + end +end diff --git a/spec/finders/personal_projects_finder_spec.rb b/spec/finders/personal_projects_finder_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..38817add45684906a556bdcaec283486345f446a --- /dev/null +++ b/spec/finders/personal_projects_finder_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe PersonalProjectsFinder do + let(:source_user) { create(:user) } + let(:current_user) { create(:user) } + + let(:finder) { described_class.new(source_user) } + + let!(:public_project) do + create(:project, :public, namespace: source_user.namespace, name: 'A', + path: 'A') + end + + let!(:private_project) do + create(:project, :private, namespace: source_user.namespace, name: 'B', + path: 'B') + end + + before do + private_project.team << [current_user, Gitlab::Access::DEVELOPER] + end + + describe 'without a current user' do + subject { finder.execute } + + it { is_expected.to eq([public_project]) } + end + + describe 'with a current user' do + subject { finder.execute(current_user) } + + it { is_expected.to eq([private_project, public_project]) } + end +end diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb index de9d4cd6128c020112ea151dc2f161d0c84306f2..d1dede78f740cd91c605e4ec1e1124c471a4f898 100644 --- a/spec/finders/projects_finder_spec.rb +++ b/spec/finders/projects_finder_spec.rb @@ -1,51 +1,56 @@ require 'spec_helper' describe ProjectsFinder do - let(:user) { create :user } - let(:group) { create :group } + describe '#execute' do + let(:user) { create(:user) } - let(:project1) { create(:empty_project, :public, group: group) } - let(:project2) { create(:empty_project, :internal, group: group) } - let(:project3) { create(:empty_project, :private, group: group) } - let(:project4) { create(:empty_project, :private, group: group) } + let!(:private_project) { create(:project, :private) } + let!(:internal_project) { create(:project, :internal) } + let!(:public_project) { create(:project, :public) } - context 'non authenticated' do - subject { ProjectsFinder.new.execute(nil, group: group) } + let(:finder) { described_class.new } - it { is_expected.to include(project1) } - it { is_expected.not_to include(project2) } - it { is_expected.not_to include(project3) } - it { is_expected.not_to include(project4) } - end + describe 'without a group' do + describe 'without a user' do + subject { finder.execute } - context 'authenticated' do - subject { ProjectsFinder.new.execute(user, group: group) } + it { is_expected.to eq([public_project]) } + end - it { is_expected.to include(project1) } - it { is_expected.to include(project2) } - it { is_expected.not_to include(project3) } - it { is_expected.not_to include(project4) } - end + describe 'with a user' do + subject { finder.execute(user) } - context 'authenticated, project member' do - before { project3.team << [user, :developer] } + describe 'without private projects' do + it { is_expected.to eq([public_project, internal_project]) } + end - subject { ProjectsFinder.new.execute(user, group: group) } + describe 'with private projects' do + before do + private_project.team.add_user(user, Gitlab::Access::MASTER) + end - it { is_expected.to include(project1) } - it { is_expected.to include(project2) } - it { is_expected.to include(project3) } - it { is_expected.not_to include(project4) } - end + it do + is_expected.to eq([public_project, internal_project, + private_project]) + end + end + end + end + + describe 'with a group' do + let(:group) { public_project.group } + + describe 'without a user' do + subject { finder.execute(nil, group: group) } - context 'authenticated, group member' do - before { group.add_developer(user) } + it { is_expected.to eq([public_project]) } + end - subject { ProjectsFinder.new.execute(user, group: group) } + describe 'with a user' do + subject { finder.execute(user, group: group) } - it { is_expected.to include(project1) } - it { is_expected.to include(project2) } - it { is_expected.to include(project3) } - it { is_expected.to include(project4) } + it { is_expected.to eq([public_project, internal_project]) } + end + end end end diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index 78a6b631eb25cb059abd0a8dfe21fc9e8a885560..1f2c4ee77b59fdf79c94f5295f3c68048f5136c3 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -127,4 +127,30 @@ describe IssuesHelper do it { is_expected.to eq("!1, !2, or !3") } end + describe "#url_to_emoji" do + it "returns url" do + expect(url_to_emoji("smile")).to include("emoji/1F604.png") + end + end + + describe "#emoji_list" do + it "returns url" do + expect(emoji_list).to be_kind_of(Array) + end + end + + describe "#note_active_class" do + before do + @note = create :note + @note1 = create :note + end + + it "returns empty string for unauthenticated user" do + expect(note_active_class(Note.all, nil)).to eq("") + end + + it "returns active string for author" do + expect(note_active_class(Note.all, @note.author)).to eq("active") + end + end end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 7d90f9877c6c6f52489774a69797f380c042ffa4..6f287719ba61c84c519b566b5ede3e685a6b7fe6 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -425,8 +425,12 @@ module Ci end describe "Error handling" do + it "fails to parse YAML" do + expect{GitlabCiYamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError) + end + it "indicates that object is invalid" do - expect{GitlabCiYamlProcessor.new("invalid_yaml\n!ccdvlf%612334@@@@")}.to raise_error(GitlabCiYamlProcessor::ValidationError) + expect{GitlabCiYamlProcessor.new("invalid_yaml")}.to raise_error(GitlabCiYamlProcessor::ValidationError) end it "returns errors if tags parameter is invalid" do diff --git a/spec/lib/gitlab/lfs/lfs_router_spec.rb b/spec/lib/gitlab/lfs/lfs_router_spec.rb index cebcb5bc8871a4cd8f34d2e0214da4afa2d59c3c..5b13d65c7c06011e1846709fd64e16ceb3b5a412 100644 --- a/spec/lib/gitlab/lfs/lfs_router_spec.rb +++ b/spec/lib/gitlab/lfs/lfs_router_spec.rb @@ -26,113 +26,52 @@ describe Gitlab::Lfs::Router do let(:sample_oid) { "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" } let(:sample_size) { 499013 } + let(:respond_with_deprecated) {[ 501, { "Content-Type"=>"application/json; charset=utf-8" }, ["{\"message\":\"Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]} + let(:respond_with_disabled) {[ 501, { "Content-Type"=>"application/json; charset=utf-8" }, ["{\"message\":\"Git LFS is not enabled on this GitLab server, contact your admin.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]} describe 'when lfs is disabled' do before do allow(Gitlab.config.lfs).to receive(:enabled).and_return(false) - env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/#{sample_oid}" + env['REQUEST_METHOD'] = 'POST' + body = { + 'objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078 + }, + { 'oid' => sample_oid, + 'size' => sample_size + } + ], + 'operation' => 'upload' + }.to_json + env['rack.input'] = StringIO.new(body) + env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/batch" end it 'responds with 501' do - respond_with_disabled = [ 501, - { "Content-Type"=>"application/vnd.git-lfs+json" }, - ["{\"message\":\"Git LFS is not enabled on this GitLab server, contact your admin.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"] - ] expect(lfs_router_auth.try_call).to match_array(respond_with_disabled) end end - describe 'when fetching lfs object' do + describe 'when fetching lfs object using deprecated API' do before do enable_lfs - env['HTTP_ACCEPT'] = "application/vnd.git-lfs+json; charset=utf-8" env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/#{sample_oid}" end - describe 'when user is authenticated' do - context 'and user has project download access' do - before do - @auth = authorize(user) - env["HTTP_AUTHORIZATION"] = @auth - project.lfs_objects << lfs_object - project.team << [user, :master] - end - - it "responds with status 200" do - expect(lfs_router_auth.try_call.first).to eq(200) - end - - it "responds with download hypermedia" do - json_response = ActiveSupport::JSON.decode(lfs_router_auth.try_call.last.first) - - expect(json_response['_links']['download']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}") - expect(json_response['_links']['download']['header']).to eq("Authorization" => @auth, "Accept" => "application/vnd.git-lfs+json; charset=utf-8") - end - end - - context 'and user does not have project access' do - it "responds with status 403" do - expect(lfs_router_auth.try_call.first).to eq(403) - end - end - end - - describe 'when user is unauthenticated' do - context 'and user does not have download access' do - it "responds with status 401" do - expect(lfs_router_noauth.try_call.first).to eq(401) - end - end - - context 'and user has download access' do - before do - project.team << [user, :master] - end - - it "responds with status 401" do - expect(lfs_router_noauth.try_call.first).to eq(401) - end - end + it 'responds with 501' do + expect(lfs_router_auth.try_call).to match_array(respond_with_deprecated) end + end - describe 'and project is public' do - context 'and project has access to the lfs object' do - before do - public_project.lfs_objects << lfs_object - end - - context 'and user is authenticated' do - it "responds with status 200 and sends download hypermedia" do - expect(lfs_router_public_auth.try_call.first).to eq(200) - json_response = ActiveSupport::JSON.decode(lfs_router_public_auth.try_call.last.first) - - expect(json_response['_links']['download']['href']).to eq("#{Gitlab.config.gitlab.url}/#{public_project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}") - expect(json_response['_links']['download']['header']).to eq("Accept" => "application/vnd.git-lfs+json; charset=utf-8") - end - end - - context 'and user is unauthenticated' do - it "responds with status 200 and sends download hypermedia" do - expect(lfs_router_public_noauth.try_call.first).to eq(200) - json_response = ActiveSupport::JSON.decode(lfs_router_public_noauth.try_call.last.first) - - expect(json_response['_links']['download']['href']).to eq("#{Gitlab.config.gitlab.url}/#{public_project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}") - expect(json_response['_links']['download']['header']).to eq("Accept" => "application/vnd.git-lfs+json; charset=utf-8") - end - end - end - - context 'and project does not have access to the lfs object' do - it "responds with status 404" do - expect(lfs_router_public_auth.try_call.first).to eq(404) - end - end + describe 'when fetching lfs object' do + before do + enable_lfs + env['HTTP_ACCEPT'] = "application/vnd.git-lfs+json; charset=utf-8" + env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}" end describe 'and request comes from gitlab-workhorse' do - before do - env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}" - end context 'without user being authorized' do it "responds with status 401" do expect(lfs_router_noauth.try_call.first).to eq(401) @@ -173,200 +112,376 @@ describe Gitlab::Lfs::Router do end end end + end - describe 'from a forked public project' do - before do - env['HTTP_ACCEPT'] = "application/vnd.git-lfs+json; charset=utf-8" - env["PATH_INFO"] = "#{forked_project.repository.path_with_namespace}.git/info/lfs/objects/#{sample_oid}" - end + describe 'when handling lfs request using deprecated API' do + before do + enable_lfs + env['REQUEST_METHOD'] = 'POST' + env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects" + end + + it 'responds with 501' do + expect(lfs_router_auth.try_call).to match_array(respond_with_deprecated) + end + end + + describe 'when handling lfs batch request' do + before do + enable_lfs + env['REQUEST_METHOD'] = 'POST' + env['PATH_INFO'] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/batch" + end + + describe 'download' do + describe 'when user is authenticated' do + before do + body = { 'operation' => 'download', + 'objects' => [ + { 'oid' => sample_oid, + 'size' => sample_size + }] + }.to_json + env['rack.input'] = StringIO.new(body) + end - context "when fetching a lfs object" do - context "and user has project download access" do + describe 'when user has download access' do before do - public_project.lfs_objects << lfs_object + @auth = authorize(user) + env["HTTP_AUTHORIZATION"] = @auth + project.team << [user, :reporter] end - it "can download the lfs object" do - expect(lfs_router_forked_auth.try_call.first).to eq(200) - json_response = ActiveSupport::JSON.decode(lfs_router_forked_auth.try_call.last.first) + context 'when downloading an lfs object that is assigned to our project' do + before do + project.lfs_objects << lfs_object + end + + it 'responds with status 200 and href to download' do + response = lfs_router_auth.try_call + expect(response.first).to eq(200) + response_body = ActiveSupport::JSON.decode(response.last.first) - expect(json_response['_links']['download']['href']).to eq("#{Gitlab.config.gitlab.url}/#{forked_project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}") - expect(json_response['_links']['download']['header']).to eq("Accept" => "application/vnd.git-lfs+json; charset=utf-8") + expect(response_body).to eq('objects' => [ + { 'oid' => sample_oid, + 'size' => sample_size, + 'actions' => { + 'download' => { + 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", + 'header' => { 'Authorization' => @auth } + } + } + }]) + end end - end - context "and user is not authenticated but project is public" do - before do - public_project.lfs_objects << lfs_object + context 'when downloading an lfs object that is assigned to other project' do + before do + public_project.lfs_objects << lfs_object + end + + it 'responds with status 200 and error message' do + response = lfs_router_auth.try_call + expect(response.first).to eq(200) + response_body = ActiveSupport::JSON.decode(response.last.first) + + expect(response_body).to eq('objects' => [ + { 'oid' => sample_oid, + 'size' => sample_size, + 'error' => { + 'code' => 404, + 'message' => "Object does not exist on the server or you don't have permissions to access it", + } + }]) + end end - it "can download the lfs object" do - expect(lfs_router_forked_auth.try_call.first).to eq(200) + context 'when downloading a lfs object that does not exist' do + before do + body = { 'operation' => 'download', + 'objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078 + }] + }.to_json + env['rack.input'] = StringIO.new(body) + end + + it "responds with status 200 and error message" do + response = lfs_router_auth.try_call + expect(response.first).to eq(200) + response_body = ActiveSupport::JSON.decode(response.last.first) + + expect(response_body).to eq('objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078, + 'error' => { + 'code' => 404, + 'message' => "Object does not exist on the server or you don't have permissions to access it", + } + }]) + end + end + + context 'when downloading one new and one existing lfs object' do + before do + body = { 'operation' => 'download', + 'objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078 + }, + { 'oid' => sample_oid, + 'size' => sample_size + } + ] + }.to_json + env['rack.input'] = StringIO.new(body) + project.lfs_objects << lfs_object + end + + it "responds with status 200 with upload hypermedia link for the new object" do + response = lfs_router_auth.try_call + expect(response.first).to eq(200) + response_body = ActiveSupport::JSON.decode(response.last.first) + + expect(response_body).to eq('objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078, + 'error' => { + 'code' => 404, + 'message' => "Object does not exist on the server or you don't have permissions to access it", + } + }, + { 'oid' => sample_oid, + 'size' => sample_size, + 'actions' => { + 'download' => { + 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", + 'header' => { 'Authorization' => @auth } + } + } + }]) + end end end - context "and user has project download access" do + context 'when user does is not member of the project' do before do - env["PATH_INFO"] = "#{forked_project.repository.path_with_namespace}.git/info/lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897" - @auth = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password) + @auth = authorize(user) env["HTTP_AUTHORIZATION"] = @auth - lfs_object_two = create(:lfs_object, :with_file, oid: "91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", size: 1575078) - public_project.lfs_objects << lfs_object_two + project.team << [user, :guest] end - it "can get a lfs object that is not in the forked project" do - expect(lfs_router_forked_auth.try_call.first).to eq(200) - - json_response = ActiveSupport::JSON.decode(lfs_router_forked_auth.try_call.last.first) - expect(json_response['_links']['download']['href']).to eq("#{Gitlab.config.gitlab.url}/#{forked_project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897") - expect(json_response['_links']['download']['header']).to eq("Accept" => "application/vnd.git-lfs+json; charset=utf-8", "Authorization" => @auth) + it 'responds with 403' do + expect(lfs_router_auth.try_call.first).to eq(403) end end - context "and user has project download access" do + context 'when user does not have download access' do before do - env["PATH_INFO"] = "#{forked_project.repository.path_with_namespace}.git/info/lfs/objects/267c8b1d876743971e3a9978405818ff5ca731c4c870b06507619cd9b1847b6b" - lfs_object_three = create(:lfs_object, :with_file, oid: "267c8b1d876743971e3a9978405818ff5ca731c4c870b06507619cd9b1847b6b", size: 127192524) - project.lfs_objects << lfs_object_three + @auth = authorize(user) + env["HTTP_AUTHORIZATION"] = @auth + project.team << [user, :guest] end - it "cannot get a lfs object that is not in the project" do - expect(lfs_router_forked_auth.try_call.first).to eq(404) + it 'responds with 403' do + expect(lfs_router_auth.try_call.first).to eq(403) end end end - end - end - - describe 'when initiating pushing of the lfs object' do - before do - enable_lfs - env['REQUEST_METHOD'] = 'POST' - env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/batch" - end - - describe 'when user is authenticated' do - before do - body = { 'objects' => [{ - 'oid' => sample_oid, - 'size' => sample_size - }] - }.to_json - env['rack.input'] = StringIO.new(body) - end - describe 'when user has project push access' do + context 'when user is not authenticated' do before do - @auth = authorize(user) - env["HTTP_AUTHORIZATION"] = @auth - project.team << [user, :master] + body = { 'operation' => 'download', + 'objects' => [ + { 'oid' => sample_oid, + 'size' => sample_size + }], + + }.to_json + env['rack.input'] = StringIO.new(body) end - context 'when pushing an lfs object that already exists' do + describe 'is accessing public project' do before do public_project.lfs_objects << lfs_object end - it "responds with status 200 and links the object to the project" do - response_body = lfs_router_auth.try_call.last - response = ActiveSupport::JSON.decode(response_body.first) + it 'responds with status 200 and href to download' do + response = lfs_router_public_noauth.try_call + expect(response.first).to eq(200) + response_body = ActiveSupport::JSON.decode(response.last.first) - expect(response['objects']).to be_kind_of(Array) - expect(response['objects'].first['oid']).to eq(sample_oid) - expect(response['objects'].first['size']).to eq(sample_size) - expect(lfs_object.projects.pluck(:id)).to_not include(project.id) - expect(lfs_object.projects.pluck(:id)).to include(public_project.id) - expect(response['objects'].first).to have_key('_links') + expect(response_body).to eq('objects' => [ + { 'oid' => sample_oid, + 'size' => sample_size, + 'actions' => { + 'download' => { + 'href' => "#{public_project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", + 'header' => {} + } + } + }]) end end - context 'when pushing a lfs object that does not exist' do + describe 'is accessing non-public project' do before do - body = { - 'objects' => [{ - 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078 - }] - }.to_json - env['rack.input'] = StringIO.new(body) + project.lfs_objects << lfs_object end - it "responds with status 200 and upload hypermedia link" do - response = lfs_router_auth.try_call - expect(response.first).to eq(200) - - response_body = ActiveSupport::JSON.decode(response.last.first) - expect(response_body['objects']).to be_kind_of(Array) - expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897") - expect(response_body['objects'].first['size']).to eq(1575078) - expect(lfs_object.projects.pluck(:id)).not_to include(project.id) - expect(response_body['objects'].first['_links']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078") - expect(response_body['objects'].first['_links']['upload']['header']).to eq("Authorization" => @auth) + it 'responds with authorization required' do + expect(lfs_router_noauth.try_call.first).to eq(401) end end + end + end - context 'when pushing one new and one existing lfs object' do + describe 'upload' do + describe 'when user is authenticated' do + before do + body = { 'operation' => 'upload', + 'objects' => [ + { 'oid' => sample_oid, + 'size' => sample_size + }] + }.to_json + env['rack.input'] = StringIO.new(body) + end + + describe 'when user has project push access' do before do - body = { - 'objects' => [ - { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', - 'size' => 1575078 - }, - { 'oid' => sample_oid, - 'size' => sample_size - } - ] - }.to_json - env['rack.input'] = StringIO.new(body) - public_project.lfs_objects << lfs_object + @auth = authorize(user) + env["HTTP_AUTHORIZATION"] = @auth + project.team << [user, :developer] end - it "responds with status 200 with upload hypermedia link for the new object" do - response = lfs_router_auth.try_call - expect(response.first).to eq(200) + context 'when pushing an lfs object that already exists' do + before do + public_project.lfs_objects << lfs_object + end - response_body = ActiveSupport::JSON.decode(response.last.first) - expect(response_body['objects']).to be_kind_of(Array) + it "responds with status 200 and links the object to the project" do + response_body = lfs_router_auth.try_call.last + response = ActiveSupport::JSON.decode(response_body.first) + expect(response['objects']).to be_kind_of(Array) + expect(response['objects'].first['oid']).to eq(sample_oid) + expect(response['objects'].first['size']).to eq(sample_size) + expect(lfs_object.projects.pluck(:id)).to_not include(project.id) + expect(lfs_object.projects.pluck(:id)).to include(public_project.id) + expect(response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}") + expect(response['objects'].first['actions']['upload']['header']).to eq('Authorization' => @auth) + end + end - expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897") - expect(response_body['objects'].first['size']).to eq(1575078) - expect(response_body['objects'].first['_links']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078") - expect(response_body['objects'].first['_links']['upload']['header']).to eq("Authorization" => @auth) + context 'when pushing a lfs object that does not exist' do + before do + body = { 'operation' => 'upload', + 'objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078 + }] + }.to_json + env['rack.input'] = StringIO.new(body) + end - expect(response_body['objects'].last['oid']).to eq(sample_oid) - expect(response_body['objects'].last['size']).to eq(sample_size) - expect(lfs_object.projects.pluck(:id)).to_not include(project.id) - expect(lfs_object.projects.pluck(:id)).to include(public_project.id) - expect(response_body['objects'].last).to have_key('_links') + it "responds with status 200 and upload hypermedia link" do + response = lfs_router_auth.try_call + expect(response.first).to eq(200) + + response_body = ActiveSupport::JSON.decode(response.last.first) + expect(response_body['objects']).to be_kind_of(Array) + expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897") + expect(response_body['objects'].first['size']).to eq(1575078) + expect(lfs_object.projects.pluck(:id)).not_to include(project.id) + expect(response_body['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078") + expect(response_body['objects'].first['actions']['upload']['header']).to eq('Authorization' => @auth) + end + end + + context 'when pushing one new and one existing lfs object' do + before do + body = { 'operation' => 'upload', + 'objects' => [ + { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', + 'size' => 1575078 + }, + { 'oid' => sample_oid, + 'size' => sample_size + } + ] + }.to_json + env['rack.input'] = StringIO.new(body) + project.lfs_objects << lfs_object + end + + it "responds with status 200 with upload hypermedia link for the new object" do + response = lfs_router_auth.try_call + expect(response.first).to eq(200) + + response_body = ActiveSupport::JSON.decode(response.last.first) + expect(response_body['objects']).to be_kind_of(Array) + + expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897") + expect(response_body['objects'].first['size']).to eq(1575078) + expect(response_body['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078") + expect(response_body['objects'].first['actions']['upload']['header']).to eq("Authorization" => @auth) + + expect(response_body['objects'].last['oid']).to eq(sample_oid) + expect(response_body['objects'].last['size']).to eq(sample_size) + expect(response_body['objects'].last).to_not have_key('actions') + end end end - end - context 'when user does not have push access' do - it 'responds with 403' do - expect(lfs_router_auth.try_call.first).to eq(403) + context 'when user does not have push access' do + it 'responds with 403' do + expect(lfs_router_auth.try_call.first).to eq(403) + end end end - end - context 'when user is not authenticated' do - context 'when user has push access' do + context 'when user is not authenticated' do before do - project.team << [user, :master] + env['rack.input'] = StringIO.new( + { 'objects' => [], 'operation' => 'upload' }.to_json + ) end - it "responds with status 401" do - expect(lfs_router_public_noauth.try_call.first).to eq(401) + context 'when user has push access' do + before do + project.team << [user, :master] + end + + it "responds with status 401" do + expect(lfs_router_public_noauth.try_call.first).to eq(401) + end end - end - context 'when user does not have push access' do - it "responds with status 401" do - expect(lfs_router_public_noauth.try_call.first).to eq(401) + context 'when user does not have push access' do + it "responds with status 401" do + expect(lfs_router_public_noauth.try_call.first).to eq(401) + end end end end + + describe 'unsupported' do + before do + body = { 'operation' => 'other', + 'objects' => [ + { 'oid' => sample_oid, + 'size' => sample_size + }] + }.to_json + env['rack.input'] = StringIO.new(body) + end + + it 'responds with status 404' do + expect(lfs_router_public_noauth.try_call.first).to eq(404) + end + end end describe 'when pushing a lfs object' do diff --git a/spec/lib/gitlab/markdown/external_link_filter_spec.rb b/spec/lib/gitlab/markdown/external_link_filter_spec.rb index a040b34577b958c033bfa8fc14d6d14877192937..bbe42a0ae18396b64c4f30ebb3cc237ba4b47e82 100644 --- a/spec/lib/gitlab/markdown/external_link_filter_spec.rb +++ b/spec/lib/gitlab/markdown/external_link_filter_spec.rb @@ -25,7 +25,15 @@ module Gitlab::Markdown doc = filter(act) expect(doc.at_css('a')).to have_attribute('rel') - expect(doc.at_css('a')['rel']).to eq 'nofollow' + expect(doc.at_css('a')['rel']).to include 'nofollow' + end + + it 'adds rel="noreferrer" to external links' do + act = %q(Google) + doc = filter(act) + + expect(doc.at_css('a')).to have_attribute('rel') + expect(doc.at_css('a')['rel']).to include 'noreferrer' end end end diff --git a/spec/lib/gitlab/push_data_builder_spec.rb b/spec/lib/gitlab/push_data_builder_spec.rb index 02710742625d18a20242d63b5f721a9b8c5ac73e..2170399ab5c180fd22b761929d156abc3743c6e2 100644 --- a/spec/lib/gitlab/push_data_builder_spec.rb +++ b/spec/lib/gitlab/push_data_builder_spec.rb @@ -17,9 +17,9 @@ describe 'Gitlab::PushDataBuilder' do it { expect(data[:repository][:git_ssh_url]).to eq(project.ssh_url_to_repo) } it { expect(data[:repository][:visibility_level]).to eq(project.visibility_level) } it { expect(data[:total_commits_count]).to eq(3) } - it { expect(data[:added]).to eq(["gitlab-grack"]) } - it { expect(data[:modified]).to eq([".gitmodules", "files/ruby/popen.rb", "files/ruby/regex.rb"]) } - it { expect(data[:removed]).to eq([]) } + it { expect(data[:commits].first[:added]).to eq(["gitlab-grack"]) } + it { expect(data[:commits].first[:modified]).to eq([".gitmodules"]) } + it { expect(data[:commits].first[:removed]).to eq([]) } end describe :build do @@ -38,8 +38,5 @@ describe 'Gitlab::PushDataBuilder' do it { expect(data[:ref]).to eq('refs/tags/v1.1.0') } it { expect(data[:commits]).to be_empty } it { expect(data[:total_commits_count]).to be_zero } - it { expect(data[:added]).to eq([]) } - it { expect(data[:modified]).to eq([]) } - it { expect(data[:removed]).to eq([]) } end end diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..9e1cd4419e0b00bdc904f3279db285706a4ae07c --- /dev/null +++ b/spec/lib/gitlab/sql/union_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe Gitlab::SQL::Union do + describe '#to_sql' do + it 'returns a String joining relations together using a UNION' do + rel1 = User.where(email: 'alice@example.com') + rel2 = User.where(email: 'bob@example.com') + union = described_class.new([rel1, rel2]) + + sql1 = rel1.reorder(nil).to_sql + sql2 = rel2.reorder(nil).to_sql + + expect(union.to_sql).to eq("#{sql1}\nUNION\n#{sql2}") + end + end +end diff --git a/spec/lib/votes_spec.rb b/spec/lib/votes_spec.rb deleted file mode 100644 index 39e5d054e62057c6249e787455257a8020710ef8..0000000000000000000000000000000000000000 --- a/spec/lib/votes_spec.rb +++ /dev/null @@ -1,188 +0,0 @@ -require 'spec_helper' - -describe Issue, 'Votes' do - let(:issue) { create(:issue) } - - describe "#upvotes" do - it "with no notes has a 0/0 score" do - expect(issue.upvotes).to eq(0) - end - - it "should recognize non-+1 notes" do - add_note "No +1 here" - expect(issue.notes.size).to eq(1) - expect(issue.notes.first.upvote?).to be_falsey - expect(issue.upvotes).to eq(0) - end - - it "should recognize a single +1 note" do - add_note "+1 This is awesome" - expect(issue.upvotes).to eq(1) - end - - it 'should recognize multiple +1 notes' do - add_note '+1 This is awesome', create(:user) - add_note '+1 I want this', create(:user) - expect(issue.upvotes).to eq(2) - end - - it 'should not count 2 +1 votes from the same user' do - add_note '+1 This is awesome' - add_note '+1 I want this' - expect(issue.upvotes).to eq(1) - end - end - - describe "#downvotes" do - it "with no notes has a 0/0 score" do - expect(issue.downvotes).to eq(0) - end - - it "should recognize non--1 notes" do - add_note "Almost got a -1" - expect(issue.notes.size).to eq(1) - expect(issue.notes.first.downvote?).to be_falsey - expect(issue.downvotes).to eq(0) - end - - it "should recognize a single -1 note" do - add_note "-1 This is bad" - expect(issue.downvotes).to eq(1) - end - - it "should recognize multiple -1 notes" do - add_note('-1 This is bad', create(:user)) - add_note('-1 Away with this', create(:user)) - expect(issue.downvotes).to eq(2) - end - end - - describe "#votes_count" do - it "with no notes has a 0/0 score" do - expect(issue.votes_count).to eq(0) - end - - it "should recognize non notes" do - add_note "No +1 here" - expect(issue.notes.size).to eq(1) - expect(issue.votes_count).to eq(0) - end - - it "should recognize a single +1 note" do - add_note "+1 This is awesome" - expect(issue.votes_count).to eq(1) - end - - it "should recognize a single -1 note" do - add_note "-1 This is bad" - expect(issue.votes_count).to eq(1) - end - - it "should recognize multiple notes" do - add_note('+1 This is awesome', create(:user)) - add_note('-1 This is bad', create(:user)) - add_note('+1 I want this', create(:user)) - expect(issue.votes_count).to eq(3) - end - - it 'should not count 2 -1 votes from the same user' do - add_note '-1 This is suspicious' - add_note '-1 This is bad' - expect(issue.votes_count).to eq(1) - end - end - - describe "#upvotes_in_percent" do - it "with no notes has a 0% score" do - expect(issue.upvotes_in_percent).to eq(0) - end - - it "should count a single 1 note as 100%" do - add_note "+1 This is awesome" - expect(issue.upvotes_in_percent).to eq(100) - end - - it 'should count multiple +1 notes as 100%' do - add_note('+1 This is awesome', create(:user)) - add_note('+1 I want this', create(:user)) - expect(issue.upvotes_in_percent).to eq(100) - end - - it 'should count fractions for multiple +1 and -1 notes correctly' do - add_note('+1 This is awesome', create(:user)) - add_note('+1 I want this', create(:user)) - add_note('-1 This is bad', create(:user)) - add_note('+1 me too', create(:user)) - expect(issue.upvotes_in_percent).to eq(75) - end - end - - describe "#downvotes_in_percent" do - it "with no notes has a 0% score" do - expect(issue.downvotes_in_percent).to eq(0) - end - - it "should count a single -1 note as 100%" do - add_note "-1 This is bad" - expect(issue.downvotes_in_percent).to eq(100) - end - - it 'should count multiple -1 notes as 100%' do - add_note('-1 This is bad', create(:user)) - add_note('-1 Away with this', create(:user)) - expect(issue.downvotes_in_percent).to eq(100) - end - - it 'should count fractions for multiple +1 and -1 notes correctly' do - add_note('+1 This is awesome', create(:user)) - add_note('+1 I want this', create(:user)) - add_note('-1 This is bad', create(:user)) - add_note('+1 me too', create(:user)) - expect(issue.downvotes_in_percent).to eq(25) - end - end - - describe '#filter_superceded_votes' do - - it 'should count a users vote only once amongst multiple votes' do - add_note('-1 This needs work before I will accept it') - add_note('+1 I want this', create(:user)) - add_note('+1 This is is awesome', create(:user)) - add_note('+1 this looks good now') - add_note('+1 This is awesome', create(:user)) - add_note('+1 me too', create(:user)) - expect(issue.downvotes).to eq(0) - expect(issue.upvotes).to eq(5) - end - - it 'should count each users vote only once' do - add_note '-1 This needs work before it will be accepted' - add_note '+1 I like this' - add_note '+1 I still like this' - add_note '+1 I really like this' - add_note '+1 Give me this now!!!!' - expect(issue.downvotes).to eq(0) - expect(issue.upvotes).to eq(1) - end - - it 'should count a users vote only once without caring about comments' do - add_note '-1 This needs work before it will be accepted' - add_note 'Comment 1' - add_note 'Another comment' - add_note '+1 vote' - add_note 'final comment' - expect(issue.downvotes).to eq(0) - expect(issue.upvotes).to eq(1) - end - - end - - def add_note(text, author = issue.author) - created_at = Time.now - 1.hour + Note.count.seconds - issue.notes << create(:note, - note: text, - project: issue.project, - author_id: author.id, - created_at: created_at) - end -end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 47863d54579e05959f2c5930d89bf2352d8f695e..d6796b07a5b3c1217d09a38a785fc3fcf362ac8f 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -77,6 +77,32 @@ describe Notify do end end + shared_examples 'it should have Gmail Actions links' do + it { is_expected.to have_body_text /ViewAction/ } + end + + shared_examples 'it should not have Gmail Actions links' do + it { is_expected.to_not have_body_text /ViewAction/ } + end + + shared_examples 'it should show Gmail Actions View Issue link' do + it_behaves_like 'it should have Gmail Actions links' + + it { is_expected.to have_body_text /View Issue/ } + end + + shared_examples 'it should show Gmail Actions View Merge request link' do + it_behaves_like 'it should have Gmail Actions links' + + it { is_expected.to have_body_text /View Merge request/ } + end + + shared_examples 'it should show Gmail Actions View Commit link' do + it_behaves_like 'it should have Gmail Actions links' + + it { is_expected.to have_body_text /View Commit/ } + end + describe 'for new users, the email' do let(:example_site_path) { root_path } let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) } @@ -87,6 +113,7 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it_behaves_like 'a new user email', new_user_address + it_behaves_like 'it should not have Gmail Actions links' it 'contains the password text' do is_expected.to have_body_text /Click here to set your password/ @@ -115,6 +142,7 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it_behaves_like 'a new user email', new_user_address + it_behaves_like 'it should not have Gmail Actions links' it 'should not contain the new user\'s password' do is_expected.not_to have_body_text /password/ @@ -127,6 +155,7 @@ describe Notify do subject { Notify.new_ssh_key_email(key.id) } it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' it 'is sent to the new user' do is_expected.to deliver_to key.user.email @@ -150,6 +179,8 @@ describe Notify do subject { Notify.new_email_email(email.id) } + it_behaves_like 'it should not have Gmail Actions links' + it 'is sent to the new user' do is_expected.to deliver_to email.user.email end @@ -194,6 +225,7 @@ describe Notify do it_behaves_like 'an assignee email' it_behaves_like 'an email starting a new thread', 'issue' + it_behaves_like 'it should show Gmail Actions View Issue link' it 'has the correct subject' do is_expected.to have_subject /#{project.name} \| #{issue.title} \(##{issue.iid}\)/ @@ -207,6 +239,8 @@ describe Notify do describe 'that are new with a description' do subject { Notify.new_issue_email(issue_with_description.assignee_id, issue_with_description.id) } + it_behaves_like 'it should show Gmail Actions View Issue link' + it 'contains the description' do is_expected.to have_body_text /#{issue_with_description.description}/ end @@ -217,6 +251,7 @@ describe Notify do it_behaves_like 'a multiple recipients email' it_behaves_like 'an answer to an existing thread', 'issue' + it_behaves_like 'it should show Gmail Actions View Issue link' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -246,6 +281,7 @@ describe Notify do subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user) } it_behaves_like 'an answer to an existing thread', 'issue' + it_behaves_like 'it should show Gmail Actions View Issue link' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -269,7 +305,6 @@ describe Notify do is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ end end - end context 'for merge requests' do @@ -282,6 +317,7 @@ describe Notify do it_behaves_like 'an assignee email' it_behaves_like 'an email starting a new thread', 'merge_request' + it_behaves_like 'it should show Gmail Actions View Merge request link' it 'has the correct subject' do is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ @@ -307,6 +343,8 @@ describe Notify do describe 'that are new with a description' do subject { Notify.new_merge_request_email(merge_request_with_description.assignee_id, merge_request_with_description.id) } + it_behaves_like 'it should show Gmail Actions View Merge request link' + it 'contains the description' do is_expected.to have_body_text /#{merge_request_with_description.description}/ end @@ -317,6 +355,7 @@ describe Notify do it_behaves_like 'a multiple recipients email' it_behaves_like 'an answer to an existing thread', 'merge_request' + it_behaves_like 'it should show Gmail Actions View Merge request link' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -346,6 +385,7 @@ describe Notify do subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user) } it_behaves_like 'an answer to an existing thread', 'merge_request' + it_behaves_like 'it should show Gmail Actions View Merge request link' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -375,6 +415,7 @@ describe Notify do it_behaves_like 'a multiple recipients email' it_behaves_like 'an answer to an existing thread', 'merge_request' + it_behaves_like 'it should show Gmail Actions View Merge request link' it 'is sent as the merge author' do sender = subject.header[:from].addrs[0] @@ -403,6 +444,7 @@ describe Notify do subject { Notify.project_was_moved_email(project.id, user.id, "gitlab/gitlab") } it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' it 'has the correct subject' do is_expected.to have_subject /Project was moved/ @@ -424,13 +466,16 @@ describe Notify do subject { Notify.project_access_granted_email(project_member.id) } it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' it 'has the correct subject' do is_expected.to have_subject /Access to project was granted/ end + it 'contains name of project' do is_expected.to have_body_text /#{project.name}/ end + it 'contains new user role' do is_expected.to have_body_text /#{project_member.human_access}/ end @@ -445,6 +490,8 @@ describe Notify do end shared_examples 'a note email' do + it_behaves_like 'it should have Gmail Actions links' + it 'is sent as the author' do sender = subject.header[:from].addrs[0] expect(sender.display_name).to eq(note_author.name) @@ -469,6 +516,7 @@ describe Notify do it_behaves_like 'a note email' it_behaves_like 'an answer to an existing thread', 'commit' + it_behaves_like 'it should show Gmail Actions View Commit link' it 'has the correct subject' do is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/ @@ -488,6 +536,7 @@ describe Notify do it_behaves_like 'a note email' it_behaves_like 'an answer to an existing thread', 'merge_request' + it_behaves_like 'it should show Gmail Actions View Merge request link' it 'has the correct subject' do is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ @@ -507,6 +556,7 @@ describe Notify do it_behaves_like 'a note email' it_behaves_like 'an answer to an existing thread', 'issue' + it_behaves_like 'it should show Gmail Actions View Issue link' it 'has the correct subject' do is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/ @@ -527,6 +577,7 @@ describe Notify do subject { Notify.group_access_granted_email(membership.id) } it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' it 'has the correct subject' do is_expected.to have_subject /Access to group was granted/ @@ -574,6 +625,8 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :create) } + it_behaves_like 'it should not have Gmail Actions links' + it 'is sent as the author' do sender = subject.header[:from].addrs[0] expect(sender.display_name).to eq(user.name) @@ -600,6 +653,8 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :create) } + it_behaves_like 'it should not have Gmail Actions links' + it 'is sent as the author' do sender = subject.header[:from].addrs[0] expect(sender.display_name).to eq(user.name) @@ -625,6 +680,8 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :delete) } + it_behaves_like 'it should not have Gmail Actions links' + it 'is sent as the author' do sender = subject.header[:from].addrs[0] expect(sender.display_name).to eq(user.name) @@ -646,6 +703,8 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) } + it_behaves_like 'it should not have Gmail Actions links' + it 'is sent as the author' do sender = subject.header[:from].addrs[0] expect(sender.display_name).to eq(user.name) @@ -671,6 +730,8 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, send_from_committer_email: send_from_committer_email) } + it_behaves_like 'it should not have Gmail Actions links' + it 'is sent as the author' do sender = subject.header[:from].addrs[0] expect(sender.display_name).to eq(user.name) @@ -774,6 +835,8 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) } + it_behaves_like 'it should show Gmail Actions View Commit link' + it 'is sent as the author' do sender = subject.header[:from].addrs[0] expect(sender.display_name).to eq(user.name) diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 90be93249511f5b8eef45d4b5d65587758c37553..0b1e2bf74d07f71666be81079abe92131f993085 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -100,4 +100,15 @@ eos # Include the subject in the repository stub. let(:extra_commits) { [subject] } end + + describe '#hook_attrs' do + let(:data) { commit.hook_attrs(with_changed_files: true) } + + it { expect(data).to be_a(Hash) } + it { expect(data[:message]).to include('Add submodule from gitlab.com') } + it { expect(data[:timestamp]).to eq('2014-02-27T11:01:38+02:00') } + it { expect(data[:added]).to eq(["gitlab-grack"]) } + it { expect(data[:modified]).to eq([".gitmodules"]) } + it { expect(data[:removed]).to eq([]) } + end end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 0f32f162a104b70ff3d124b0cf9fcece128b6b94..ae53f7a536b25638795a87bf8cda1ebe48acb19f 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -64,4 +64,42 @@ describe Event do it { expect(@event.branch_name).to eq("master") } it { expect(@event.author).to eq(@user) } end + + describe '.latest_update_time' do + describe 'when events are present' do + let(:time) { Time.utc(2015, 1, 1) } + + before do + create(:closed_issue_event, updated_at: time) + create(:closed_issue_event, updated_at: time + 5) + end + + it 'returns the latest update time' do + expect(Event.latest_update_time).to eq(time + 5) + end + end + + describe 'when no events exist' do + it 'returns nil' do + expect(Event.latest_update_time).to be_nil + end + end + end + + describe '.limit_recent' do + let!(:event1) { create(:closed_issue_event) } + let!(:event2) { create(:closed_issue_event) } + + describe 'without an explicit limit' do + subject { Event.limit_recent } + + it { is_expected.to eq([event2, event1]) } + end + + describe 'with an explicit limit' do + subject { Event.limit_recent(1) } + + it { is_expected.to eq([event2]) } + end + end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index bbfc5535eec532794f2be9e720c8f0fe420663e7..6f166b5ab7527c60f3442e513085603a8d6a74e0 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -38,6 +38,33 @@ describe Group do it { is_expected.not_to validate_presence_of :owner } end + describe '.public_and_given_groups' do + let!(:public_group) { create(:group, public: true) } + + subject { described_class.public_and_given_groups([group.id]) } + + it { is_expected.to eq([public_group, group]) } + end + + describe '.visible_to_user' do + let!(:group) { create(:group) } + let!(:user) { create(:user) } + + subject { described_class.visible_to_user(user) } + + describe 'when the user has access to a group' do + before do + group.add_user(user, Gitlab::Access::MASTER) + end + + it { is_expected.to eq([group]) } + end + + describe 'when the user does not have access to any groups' do + it { is_expected.to eq([]) } + end + end + describe '#to_reference' do it 'returns a String reference to the object' do expect(group.to_reference).to eq "@#{group.name}" diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 90af75ff0e3ff84dce44a33e7d00c4d6898edfef..567c911425c8d526e67c962e8d5f1b59a6d2cb8e 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -193,4 +193,29 @@ describe MergeRequest do it_behaves_like 'a Taskable' do subject { create :merge_request, :simple } end + + describe '#ci_commit' do + describe 'when the source project exists' do + it 'returns the latest commit' do + commit = double(:commit, id: '123abc') + ci_commit = double(:ci_commit) + + allow(subject).to receive(:last_commit).and_return(commit) + + expect(subject.source_project).to receive(:ci_commit). + with('123abc'). + and_return(ci_commit) + + expect(subject.ci_commit).to eq(ci_commit) + end + end + + describe 'when the source project does not exist' do + it 'returns nil' do + allow(subject).to receive(:source_project).and_return(nil) + + expect(subject.ci_commit).to be_nil + end + end + end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 75564839dcfb6baac5c2b44357b1184cd5fc0b73..f347f537550814028dccc8f35e141a3cfb5495f4 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -32,77 +32,6 @@ describe Note do it { is_expected.to validate_presence_of(:project) } end - describe '#votable?' do - it 'is true for issue notes' do - note = build(:note_on_issue) - expect(note).to be_votable - end - - it 'is true for merge request notes' do - note = build(:note_on_merge_request) - expect(note).to be_votable - end - - it 'is false for merge request diff notes' do - note = build(:note_on_merge_request_diff) - expect(note).not_to be_votable - end - - it 'is false for commit notes' do - note = build(:note_on_commit) - expect(note).not_to be_votable - end - - it 'is false for commit diff notes' do - note = build(:note_on_commit_diff) - expect(note).not_to be_votable - end - end - - describe 'voting score' do - it 'recognizes a neutral note' do - note = build(:votable_note, note: 'This is not a +1 note') - expect(note).not_to be_upvote - expect(note).not_to be_downvote - end - - it 'recognizes a neutral emoji note' do - note = build(:votable_note, note: "I would :+1: this, but I don't want to") - expect(note).not_to be_upvote - expect(note).not_to be_downvote - end - - it 'recognizes a +1 note' do - note = build(:votable_note, note: '+1 for this') - expect(note).to be_upvote - end - - it 'recognizes a +1 emoji as a vote' do - note = build(:votable_note, note: ':+1: for this') - expect(note).to be_upvote - end - - it 'recognizes a thumbsup emoji as a vote' do - note = build(:votable_note, note: ':thumbsup: for this') - expect(note).to be_upvote - end - - it 'recognizes a -1 note' do - note = build(:votable_note, note: '-1 for this') - expect(note).to be_downvote - end - - it 'recognizes a -1 emoji as a vote' do - note = build(:votable_note, note: ':-1: for this') - expect(note).to be_downvote - end - - it 'recognizes a thumbsdown emoji as a vote' do - note = build(:votable_note, note: ':thumbsdown: for this') - expect(note).to be_downvote - end - end - describe "Commit notes" do let!(:note) { create(:note_on_commit, note: "+1 from me") } let!(:commit) { note.noteable } @@ -139,10 +68,6 @@ describe Note do it "should be recognized by #for_commit_diff_line?" do expect(note).to be_for_commit_diff_line end - - it "should not be votable" do - expect(note).not_to be_votable - end end describe 'authorization' do @@ -204,4 +129,16 @@ describe Note do it { expect(Note.search('wow')).to include(note) } end + + describe :grouped_awards do + before do + create :note, note: "smile", is_award: true + create :note, note: "smile", is_award: true + end + + it "returns grouped array of notes" do + expect(Note.grouped_awards.first.first).to eq("smile") + expect(Note.grouped_awards.first.last).to match_array(Note.all) + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 8d7e6e76766645934e6830f9bc8a9a690bc63e09..f80fada45e9446b5d17bb3918f19833c2db8ff56 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -345,17 +345,6 @@ describe Project do expect(project1.star_count).to eq(0) expect(project2.star_count).to eq(0) end - - it 'is decremented when an upvoter account is deleted' do - user = create :user - project = create :project, :public - user.toggle_star(project) - project.reload - expect(project.star_count).to eq(1) - user.destroy - project.reload - expect(project.star_count).to eq(0) - end end describe :avatar_type do @@ -464,4 +453,23 @@ describe Project do end end end + + describe '.visible_to_user' do + let!(:project) { create(:project, :private) } + let!(:user) { create(:user) } + + subject { described_class.visible_to_user(user) } + + describe 'when a user has access to a project' do + before do + project.team.add_user(user, Gitlab::Access::MASTER) + end + + it { is_expected.to eq([project]) } + end + + describe 'when a user does not have access to any projects' do + it { is_expected.to eq([]) } + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 7d716c231206139e11641cb15c313c841b7e298b..4631b12faf1763f79547e1c9daf5e34e32466d0d 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -686,7 +686,7 @@ describe User do end end - describe "#contributed_projects_ids" do + describe "#contributed_projects" do subject { create(:user) } let!(:project1) { create(:project) } let!(:project2) { create(:project, forked_from_project: project3) } @@ -701,15 +701,15 @@ describe User do end it "includes IDs for projects the user has pushed to" do - expect(subject.contributed_projects_ids).to include(project1.id) + expect(subject.contributed_projects).to include(project1) end it "includes IDs for projects the user has had merge requests merged into" do - expect(subject.contributed_projects_ids).to include(project3.id) + expect(subject.contributed_projects).to include(project3) end it "doesn't include IDs for unrelated projects" do - expect(subject.contributed_projects_ids).not_to include(project2.id) + expect(subject.contributed_projects).not_to include(project2) end end @@ -758,4 +758,30 @@ describe User do expect(subject.recent_push).to eq(nil) end end + + describe '#authorized_groups' do + let!(:user) { create(:user) } + let!(:private_group) { create(:group) } + + before do + private_group.add_user(user, Gitlab::Access::MASTER) + end + + subject { user.authorized_groups } + + it { is_expected.to eq([private_group]) } + end + + describe '#authorized_projects' do + let!(:user) { create(:user) } + let!(:private_project) { create(:project, :private) } + + before do + private_project.team << [user, Gitlab::Access::MASTER] + end + + subject { user.authorized_projects } + + it { is_expected.to eq([private_project]) } + end end diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 8b177af4689b09b63ff434c891d7d4feebdfa358..94c843d0ce9eabebd7dfe3179f36ed1c218780a9 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) } @@ -22,7 +22,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 end @@ -65,7 +65,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 end @@ -78,7 +78,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 @@ -122,6 +122,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 @@ -142,7 +163,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 @@ -164,7 +185,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 @@ -179,10 +200,9 @@ 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 end - end diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index 606b226ad770f0f5de622b5fc8dedc1119d3745f..7abd9ea81f5d7e00715f41a02bcd3b1e57070611 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -140,14 +140,24 @@ describe API::API, 'ProjectHooks', api: true do expect(response.status).to eq(200) end - it "should return success when deleting non existent hook" do + it "should return a 404 error when deleting non existent hook" do delete api("/projects/#{project.id}/hooks/42", user) - expect(response.status).to eq(200) + expect(response.status).to eq(404) end it "should return a 405 error if hook id not given" do delete api("/projects/#{project.id}/hooks", user) expect(response.status).to eq(405) end + + it "shold return a 404 if a user attempts to delete project hooks he/she does not own" do + test_user = create(:user) + other_project = create(:project) + other_project.team << [test_user, :master] + + delete api("/projects/#{other_project.id}/hooks/#{hook.id}", test_user) + expect(response.status).to eq(404) + expect(WebHook.exists?(hook.id)).to be_truthy + end end end diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..54e3d77ae35538a49df233b5fe8c7ad381907587 --- /dev/null +++ b/spec/requests/api/project_snippets_spec.rb @@ -0,0 +1,105 @@ +require 'rails_helper' + +describe API::API, api: true do + include ApiHelpers + + describe 'GET /projects/:project_id/snippets/:id' do + # TODO (rspeicher): Deprecated; remove in 9.0 + it 'always exposes expires_at as nil' do + admin = create(:admin) + snippet = create(:project_snippet, author: admin) + + get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin) + + expect(json_response).to have_key('expires_at') + 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, visibility_level: 20, project: project) + internal_snippet = create(:project_snippet, visibility_level: 10, project: project) + private_snippet = create(:project_snippet, visibility_level: 0, 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, visibility_level: 0, 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 9fc294118ae12e35fee731274c5c4dbcf16cbefa..88804987859b73b61f9e4ba16fe113ad2b9c8528 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -11,9 +11,9 @@ 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(:project_member) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } - let(:project_member2) { create(:project_member, user: user3, project: project, access_level: ProjectMember::DEVELOPER) } + let(:snippet) { create(:project_snippet, visibility_level: 20, author: user, project: project, title: 'example') } + let(:project_member) { create(:project_member, access_level: ProjectMember::MASTER, user: user, project: project) } + let(:project_member2) { create(:project_member, access_level: ProjectMember::DEVELOPER, user: user3, project: project) } let(:user4) { create(:user) } let(:project3) do create(:project, diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index cc9a5f4758245b7fbc1f9c57d912c6104a86a826..17f2643fd45f25ad1f7076ba363f1ae76699dbd1 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -28,10 +28,10 @@ describe API::API, api: true do before do release = project.releases.find_or_initialize_by(tag: tag_name) release.update_attributes(description: description) - get api("/projects/#{project.id}/repository/tags", user) end it "should return an array of project tags with release info" do + get api("/projects/#{project.id}/repository/tags", user) expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.first['name']).to eq(tag_name) @@ -119,17 +119,78 @@ describe API::API, api: true do end end - describe 'PUT /projects/:id/repository/:tag/release' do + describe 'POST /projects/:id/repository/tags/:tag_name/release' do let(:tag_name) { project.repository.tag_names.first } let(:description) { 'Awesome release!' } it 'should create description for existing git tag' do - put api("/projects/#{project.id}/repository/#{tag_name}/release", user), + post api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user), description: description - expect(response.status).to eq(200) - expect(json_response['tag']).to eq(tag_name) + expect(response.status).to eq(201) + expect(json_response['tag_name']).to eq(tag_name) expect(json_response['description']).to eq(description) end + + it 'should return 404 if the tag does not exist' do + post api("/projects/#{project.id}/repository/tags/foobar/release", user), + description: description + + expect(response.status).to eq(404) + expect(json_response['message']).to eq('Tag does not exist') + end + + context 'on tag with existing release' do + before do + release = project.releases.find_or_initialize_by(tag: tag_name) + release.update_attributes(description: description) + end + + it 'should return 409 if there is already a release' do + post api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user), + description: description + + expect(response.status).to eq(409) + expect(json_response['message']).to eq('Release already exists') + end + end + end + + describe 'PUT id/repository/tags/:tag_name/release' do + let(:tag_name) { project.repository.tag_names.first } + let(:description) { 'Awesome release!' } + let(:new_description) { 'The best release!' } + + context 'on tag with existing release' do + before do + release = project.releases.find_or_initialize_by(tag: tag_name) + release.update_attributes(description: description) + end + + it 'should update the release description' do + put api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user), + description: new_description + + expect(response.status).to eq(200) + expect(json_response['tag_name']).to eq(tag_name) + expect(json_response['description']).to eq(new_description) + end + end + + it 'should return 404 if the tag does not exist' do + put api("/projects/#{project.id}/repository/tags/foobar/release", user), + description: new_description + + expect(response.status).to eq(404) + expect(json_response['message']).to eq('Tag does not exist') + end + + it 'should return 404 if the release does not exist' do + put api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user), + description: new_description + + expect(response.status).to eq(404) + expect(json_response['message']).to eq('Release does not exist') + end end end diff --git a/spec/services/ci/create_commit_service_spec.rb b/spec/services/ci/create_commit_service_spec.rb index e3a8fe9681b6c87a84c1ff7732deae4d8f9be062..e0ede1d58b71a300b8902afcadfe39e59c892ae1 100644 --- a/spec/services/ci/create_commit_service_spec.rb +++ b/spec/services/ci/create_commit_service_spec.rb @@ -53,7 +53,7 @@ module Ci end end - it 'fails commits without .gitlab-ci.yml' do + it 'skips commits without .gitlab-ci.yml' do stub_ci_commit_yaml_file(nil) result = service.execute(project, user, ref: 'refs/heads/0_1', @@ -63,7 +63,24 @@ module Ci ) expect(result).to be_persisted expect(result.builds.any?).to be_falsey - expect(result.status).to eq('failed') + expect(result.status).to eq('skipped') + expect(result.yaml_errors).to be_nil + end + + it 'skips commits if yaml is invalid' do + message = 'message' + allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { message } + stub_ci_commit_yaml_file('invalid: file: file') + commits = [{ message: message }] + commit = service.execute(project, user, + ref: 'refs/tags/0_1', + before: '00000000', + after: '31das312', + commits: commits + ) + expect(commit.builds.any?).to be false + expect(commit.status).to eq('failed') + expect(commit.yaml_errors).to_not be_nil end describe :ci_skip? do @@ -100,7 +117,7 @@ module Ci end it "skips builds creation if there is [ci skip] tag in commit message and yaml is invalid" do - stub_ci_commit_yaml_file('invalid: file') + stub_ci_commit_yaml_file('invalid: file: fiile') commits = [{ message: message }] commit = service.execute(project, user, ref: 'refs/tags/0_1', @@ -110,6 +127,7 @@ module Ci ) expect(commit.builds.any?).to be false expect(commit.status).to eq("skipped") + expect(commit.yaml_errors).to be_nil end end diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb index 4c62fbafd733f147ae1b28739aeab4439e2754c9..93ed52c65ccb6c427406a66294c2f62d0d156887 100644 --- a/spec/services/issues/bulk_update_service_spec.rb +++ b/spec/services/issues/bulk_update_service_spec.rb @@ -100,7 +100,7 @@ describe Issues::BulkUpdateService do describe :update_milestone do before do - @milestone = create :milestone + @milestone = create(:milestone, project: @project) @params = { issues_ids: [issue.id], milestone_id: @milestone.id diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 7f1ebcb319897ce69b965524670ddadbc385666d..5a811ce0acebd4084ff937d4ec47516b2a55b641 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -4,20 +4,60 @@ describe Issues::CreateService do let(:project) { create(:empty_project) } let(:user) { create(:user) } - describe :execute do - context "valid params" do + describe '#execute' do + let(:issue) { described_class.new(project, user, opts).execute } + + context 'when params are valid' do + let(:assignee) { create(:user) } + let(:milestone) { create(:milestone, project: project) } + let(:labels) { [create(:label, title: 'foo', project: project), create(:label, title: 'bar', project: project)] } + before do project.team << [user, :master] - opts = { - title: 'Awesome issue', - description: 'please fix' - } + project.team << [assignee, :master] + end - @issue = Issues::CreateService.new(project, user, opts).execute + let(:opts) do + { title: 'Awesome issue', + description: 'please fix', + assignee: assignee, + label_ids: labels.map(&:id), + milestone_id: milestone.id } end - it { expect(@issue).to be_valid } - it { expect(@issue.title).to eq('Awesome issue') } + it { expect(issue).to be_valid } + it { expect(issue.title).to eq('Awesome issue') } + it { expect(issue.assignee).to eq assignee } + it { expect(issue.labels).to match_array labels } + it { expect(issue.milestone).to eq milestone } + + context 'when label belongs to different project' do + let(:label) { create(:label) } + + let(:opts) do + { title: 'Title', + description: 'Description', + label_ids: [label.id] } + end + + it 'does not assign label'do + expect(issue.labels).to_not include label + end + end + + context 'when milestone belongs to different project' do + let(:milestone) { create(:milestone) } + + let(:opts) do + { title: 'Title', + description: 'Description', + milestone_id: milestone.id } + end + + it 'does not assign milestone' do + expect(issue.milestone).to_not eq milestone + end + end end end end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index a91be3b44721541e30c760509925945bbc3c5803..55c04b6e42cd251d649497454e77cd1c5e4ceb42 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -3,13 +3,21 @@ require 'spec_helper' describe Issues::UpdateService do let(:user) { create(:user) } let(:user2) { create(:user) } - let(:issue) { create(:issue, title: 'Old title') } - let(:label) { create(:label) } - let(:project) { issue.project } + let(:user3) { create(:user) } + let(:project) { create(:empty_project) } + let(:label) { create(:label, project: project) } + let(:label2) { create(:label) } + + let(:issue) do + create(:issue, title: 'Old title', + assignee_id: user3.id, + project: project) + end before do project.team << [user, :master] project.team << [user2, :developer] + project.team << [user3, :developer] end describe 'execute' do @@ -34,9 +42,11 @@ describe Issues::UpdateService do it { expect(@issue.labels.count).to eq(1) } it { expect(@issue.labels.first.title).to eq('Bug') } - it 'should send email to user2 about assign of new issue' do - email = ActionMailer::Base.deliveries.last - expect(email.to.first).to eq(user2.email) + it 'should send email to user2 about assign of new issue and email to user3 about issue unassignment' do + deliveries = ActionMailer::Base.deliveries + email = deliveries.last + recipients = deliveries.last(2).map(&:to).flatten + expect(recipients).to include(user2.email, user3.email) expect(email.subject).to include(issue.title) end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index c75173c1452002a23d92fd0419d61b0764082531..08aa4495d30740ae1c00e0bbc4ad43efcaa99914 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -1,11 +1,18 @@ require 'spec_helper' -describe MergeRequests::UpdateService do +describe MergeRequests::UpdateService, services: true do + let(:project) { create(:project) } let(:user) { create(:user) } let(:user2) { create(:user) } - let(:merge_request) { create(:merge_request, :simple, title: 'Old title') } - let(:project) { merge_request.project } - let(:label) { create(:label) } + let(:user3) { create(:user) } + let(:label) { create(:label, project: project) } + let(:label2) { create(:label) } + + let(:merge_request) do + create(:merge_request, :simple, title: 'Old title', + assignee_id: user3.id, + source_project: project) + end before do project.team << [user, :master] @@ -47,9 +54,11 @@ describe MergeRequests::UpdateService do with(@merge_request, 'update') end - it 'should send email to user2 about assign of new merge_request' do - email = ActionMailer::Base.deliveries.last - expect(email.to.first).to eq(user2.email) + it 'should send email to user2 about assign of new merge request and email to user3 about merge request unassignment' do + deliveries = ActionMailer::Base.deliveries + email = deliveries.last + recipients = deliveries.last(2).map(&:to).flatten + expect(recipients).to include(user2.email, user3.email) expect(email.subject).to include(merge_request.title) end diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index f2ea0805b2fb8466353c0200ccf2e4a8eeb2871e..cc38d2577928e041ff7788df15d3fa9e244560bb 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -24,4 +24,38 @@ describe Notes::CreateService do it { expect(@note.note).to eq('Awesome comment') } end end + + describe "award emoji" do + before do + project.team << [user, :master] + end + + it "creates emoji note" do + opts = { + note: ':smile: ', + noteable_type: 'Issue', + noteable_id: issue.id + } + + @note = Notes::CreateService.new(project, user, opts).execute + + expect(@note).to be_valid + expect(@note.note).to eq('smile') + expect(@note.is_award).to be_truthy + end + + it "creates regular note if emoji name is invalid" do + opts = { + note: ':smile: moretext: ', + noteable_type: 'Issue', + noteable_id: issue.id + } + + @note = Notes::CreateService.new(project, user, opts).execute + + expect(@note).to be_valid + expect(@note.note).to eq(opts[:note]) + expect(@note.is_award).to be_falsy + end + end end diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index e397b2b9b4a0133d0f419df317931911f55f2aa8..1feba6ce0489e4cec3a31ed0f73b7a533b05d2bd 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -25,13 +25,6 @@ describe Projects::ForkService do end end - context 'fork project failure' do - it "fails due to transaction failure" do - @to_project = fork_project(@from_project, @to_user, false) - expect(@to_project.import_failed?) - end - end - context 'project already exists' do it "should fail due to validation, not transaction failure" do @existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace) @@ -66,7 +59,7 @@ describe Projects::ForkService do context 'fork project for group' do it 'group owner successfully forks project into the group' do - to_project = fork_project(@project, @group_owner, true, @opts) + to_project = fork_project(@project, @group_owner, @opts) expect(to_project.owner).to eq(@group) expect(to_project.namespace).to eq(@group) expect(to_project.name).to eq(@project.name) @@ -78,7 +71,7 @@ describe Projects::ForkService do context 'fork project for group when user not owner' do it 'group developer should fail to fork project into the group' do - to_project = fork_project(@project, @developer, true, @opts) + to_project = fork_project(@project, @developer, @opts) expect(to_project.errors[:namespace]).to eq(['is not valid']) end end @@ -87,7 +80,7 @@ describe Projects::ForkService do it 'should fail due to validation, not transaction failure' do existing_project = create(:project, name: @project.name, namespace: @group) - to_project = fork_project(@project, @group_owner, true, @opts) + to_project = fork_project(@project, @group_owner, @opts) expect(existing_project.persisted?).to be_truthy expect(to_project.errors[:name]).to eq(['has already been taken']) expect(to_project.errors[:path]).to eq(['has already been taken']) @@ -95,8 +88,8 @@ describe Projects::ForkService do end end - def fork_project(from_project, user, fork_success = true, params = {}) - allow(RepositoryForkWorker).to receive(:perform_async).and_return(fork_success) + def fork_project(from_project, user, params = {}) + allow(RepositoryForkWorker).to receive(:perform_async).and_return(true) Projects::ForkService.new(from_project, user, params).execute end end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 787670e929792e5080975af31f4b5dc50dd96cf0..78b9a0f42fa6dfb80594bba2778be67ac4e6c775 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -21,7 +21,8 @@ module TestEnv # We currently only need a subset of the branches FORKED_BRANCH_SHA = { 'add-submodule-version-bump' => '3f547c08', - 'master' => '5937ac0' + 'master' => '5937ac0', + 'remove-submodule' => '2a33e0c0' } # Test environment diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 06559c3925d5b8e08e9c0da253210db2b8d92355..63bed2414dfbcfac332e615889ecaa753ec6328c 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -16,7 +16,7 @@ describe 'gitlab:app namespace rake task' do end def reenable_backup_sub_tasks - %w{db repo uploads builds artifacts}.each do |subtask| + %w{db repo uploads builds artifacts lfs}.each do |subtask| Rake::Task["gitlab:backup:#{subtask}:create"].reenable end end @@ -49,7 +49,7 @@ describe 'gitlab:app namespace rake task' do to raise_error(SystemExit) end - it 'should invoke restoration on mach' do + it 'should invoke restoration on match' do allow(YAML).to receive(:load_file). and_return({ gitlab_version: gitlab_version }) expect(Rake::Task["gitlab:backup:db:restore"]).to receive(:invoke) @@ -57,6 +57,7 @@ describe 'gitlab:app namespace rake task' do expect(Rake::Task["gitlab:backup:builds:restore"]).to receive(:invoke) expect(Rake::Task["gitlab:backup:uploads:restore"]).to receive(:invoke) expect(Rake::Task["gitlab:backup:artifacts:restore"]).to receive(:invoke) + expect(Rake::Task["gitlab:backup:lfs:restore"]).to receive(:invoke) expect(Rake::Task["gitlab:shell:setup"]).to receive(:invoke) expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error end @@ -114,7 +115,7 @@ describe 'gitlab:app namespace rake task' do it 'should set correct permissions on the tar contents' do tar_contents, exit_status = Gitlab::Popen.popen( - %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz} + %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz} ) expect(exit_status).to eq(0) expect(tar_contents).to match('db/') @@ -122,12 +123,13 @@ describe 'gitlab:app namespace rake task' do expect(tar_contents).to match('repositories/') expect(tar_contents).to match('builds.tar.gz') expect(tar_contents).to match('artifacts.tar.gz') + expect(tar_contents).to match('lfs.tar.gz') expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz)\/$/) end it 'should delete temp directories' do temp_dirs = Dir.glob( - File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts}') + File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs}') ) expect(temp_dirs).to be_empty @@ -149,7 +151,7 @@ describe 'gitlab:app namespace rake task' do # Redirect STDOUT and run the rake task orig_stdout = $stdout $stdout = StringIO.new - ENV["SKIP"] = "repositories" + ENV["SKIP"] = "repositories,uploads" run_rake_task('gitlab:backup:create') $stdout = orig_stdout @@ -163,13 +165,14 @@ describe 'gitlab:app namespace rake task' do it "does not contain skipped item" do tar_contents, _exit_status = Gitlab::Popen.popen( - %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz} + %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz} ) expect(tar_contents).to match('db/') expect(tar_contents).to match('uploads.tar.gz') expect(tar_contents).to match('builds.tar.gz') expect(tar_contents).to match('artifacts.tar.gz') + expect(tar_contents).to match('lfs.tar.gz') expect(tar_contents).not_to match('repositories/') end @@ -180,8 +183,10 @@ describe 'gitlab:app namespace rake task' do expect(Rake::Task["gitlab:backup:db:restore"]).to receive :invoke expect(Rake::Task["gitlab:backup:repo:restore"]).not_to receive :invoke + expect(Rake::Task["gitlab:backup:uploads:restore"]).not_to receive :invoke expect(Rake::Task["gitlab:backup:builds:restore"]).to receive :invoke expect(Rake::Task["gitlab:backup:artifacts:restore"]).to receive :invoke + expect(Rake::Task["gitlab:backup:lfs:restore"]).to receive :invoke expect(Rake::Task["gitlab:shell:setup"]).to receive :invoke expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error end diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb index aa0311069689dd191102a093a473febbfeefce72..245f066df1f6a016baa738e15e3c1642133e9a6f 100644 --- a/spec/workers/repository_fork_worker_spec.rb +++ b/spec/workers/repository_fork_worker_spec.rb @@ -12,7 +12,6 @@ describe RepositoryForkWorker do project.path_with_namespace, fork_project.namespace.path). and_return(true) - expect(ProjectCacheWorker).to receive(:perform_async) subject.perform(project.id, project.path_with_namespace,