diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 76117a48730..474bf6765f4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -210,7 +210,7 @@ rake brakeman: *exec rake flay: *exec license_finder: *exec rake downtime_check: *exec -rake ce_to_ee_merge_check: +rake ee_compat_check: <<: *exec only: - branches @@ -279,16 +279,20 @@ bundler:audit: migration paths: stage: test <<: *use-db + variables: + SETUP_DB: "false" only: - master@gitlab-org/gitlab-ce script: - git checkout HEAD . - git fetch --tags - git checkout v8.5.9 - - 'echo test: unix:/var/opt/gitlab/redis/redis.socket > config/resque.yml' + - cp config/resque.yml.example config/resque.yml + - sed -i 's/localhost/redis/g' config/resque.yml - bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" --retry=3 - rake db:drop db:create db:schema:load db:seed_fu - git checkout $CI_BUILD_REF + - source scripts/prepare_build.sh - rake db:migrate coverage: diff --git a/.rubocop.yml b/.rubocop.yml index bec2464c740..13df3f99613 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -5,7 +5,7 @@ require: inherit_from: .rubocop_todo.yml AllCops: - TargetRubyVersion: 2.3 + TargetRubyVersion: 2.1 # Cop names are not d§splayed in offense messages by default. Change behavior # by overriding DisplayCopNames, or by giving the -D/--display-cop-names # option. diff --git a/.scss-lint.yml b/.scss-lint.yml index 5093702519b..5c8e5ac0758 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -172,7 +172,7 @@ linters: # Split selectors onto separate lines after each comma, and have each # individual selector occupy a single line. SingleLinePerSelector: - enabled: false + enabled: true # Commas in lists should be followed by a space. SpaceAfterComma: @@ -191,7 +191,7 @@ linters: # Variables should be formatted with a single space separating the colon # from the variable's value. SpaceAfterVariableColon: - enabled: false + enabled: true # Variables should be formatted with no space between the name and the # colon. @@ -201,7 +201,7 @@ linters: # Operators should be formatted with a single space on both sides of an # infix operator. SpaceAroundOperator: - enabled: false + enabled: true # Opening braces should be preceded by a single space. SpaceBeforeBrace: @@ -223,7 +223,7 @@ linters: # Reports lines containing trailing whitespace. TrailingWhitespace: - enabled: false + enabled: true # Don't write trailing zeros for numeric values with a decimal point. TrailingZero: diff --git a/CHANGELOG.md b/CHANGELOG.md index 399a7f65267..92c730eaa8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,39 @@ Please view this file on the master branch, on stable branches it's out of date. ## 8.14.0 (2016-11-22) - Adds user project membership expired event to clarify why user was removed (Callum Dryden) - - Simpler arguments passed to named_route on toggle_award_url helper method + - Trim leading and trailing whitespace on project_path (Linus Thiel) + - Prevent award emoji via notes for issues/MRs authored by user (barthc) + - Adds an optional path parameter to the Commits API to filter commits by path (Luis HGO) + - Fix extra space on Build sidebar on Firefox !7060 + - Fix HipChat notifications rendering (airatshigapov, eisnerd) + - Add hover to trash icon in notes !7008 (blackst0ne) + - Escape ref and path for relative links !6050 (winniehell) + - Simpler arguments passed to named_route on toggle_award_url helper method + - Fix: Backup restore doesn't clear cache + - Replace jquery.cookie plugin with js.cookie !7085 + - Use MergeRequestsClosingIssues cache data on Issue#closed_by_merge_requests method + - Fix Sign in page 'Forgot your password?' link overlaps on medium-large screens + - Fix documents and comments on Build API `scope` + - Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov) + +## 8.13.1 (unreleased) + - Fix bug where labels would be assigned to issues that were moved + - Fix error in generating labels + - Fix reply-by-email not working due to queue name mismatch + - Fixed hidden pipeline graph on commit and MR page !6895 + - Expire and build repository cache after project import + - Reduce overhead of LabelFinder by avoiding #presence call !7094 + - Fix 404 for group pages when GitLab setup uses relative url + - Simpler arguments passed to named_route on toggle_award_url helper method + - Fix unauthorized users dragging on issue boards + - Better handle when no users were selected for adding to group or project. (Linus Thiel) + - Only show register tab if signup enabled. + - Only schedule ProjectCacheWorker jobs when needed ## 8.13.0 (2016-10-22) - + - Removes extra line for empty issue description. (!7045) - Fix save button on project pipeline settings page. (!6955) + - All Sidekiq workers now use their own queue - Avoid race condition when asynchronously removing expired artifacts. (!6881) - Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675) - Respond with 404 Not Found for non-existent tags (Linus Thiel) @@ -22,27 +50,35 @@ Please view this file on the master branch, on stable branches it's out of date. - Add `/projects/visible` API endpoint (Ben Boeckel) - Fix centering of custom header logos (Ashley Dumaine) - Keep around commits only pipeline creation as pipeline data doesn't change over time + - Update duration at the end of pipeline - ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup - Add group level labels. (!6425) + - Fix Cycle analytics not showing correct data when filtering by date. !6906 - Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun) - Cancelled pipelines could be retried. !6927 - Updating verbiage on git basics to be more intuitive + - Fix project_feature record not generated on project creation - Clarify documentation for Runners API (Gennady Trafimenkov) - The instrumentation for Banzai::Renderer has been restored - Change user & group landing page routing from /u/:username to /:username + - Fixed issue boards user link when in subdirectory - Added documentation for .gitattributes files - Move Pipeline Metrics to separate worker - AbstractReferenceFilter caches project_refs on RequestStore when active - Replaced the check sign to arrow in the show build view. !6501 - Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar) + - ProjectCacheWorker updates caches at most once per 15 minutes per project - Fix Error 500 when viewing old merge requests with bad diff data - Create a new /templates namespace for the /licenses, /gitignores and /gitlab_ci_ymls API endpoints. !5717 (tbalthazar) + - Fix viewing merged MRs when the source project has been removed !6991 - Speed-up group milestones show page - Fix inconsistent options dropdown caret on mobile viewports (ClemMakesApps) - Extract project#update_merge_requests and SystemHooks to its own worker from GitPushService + - Fix discussion thread from emails for merge requests. !7010 - Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs) - Add tag shortcut from the Commit page. !6543 - Keep refs for each deployment + - Close open tooltips on page navigation (Linus Thiel) - Allow browsing branches that end with '.atom' - Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller) - Replace unique keyframes mixin with keyframe mixin with specific names (ClemMakesApps) @@ -70,12 +106,14 @@ Please view this file on the master branch, on stable branches it's out of date. - Add RTL support to markdown renderer (Ebrahim Byagowi) - Add word-wrap to issue title on issue and milestone boards (ClemMakesApps) - Fix todos page mobile viewport layout (ClemMakesApps) + - Make issues search less finicky - Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps) - Remove redundant mixins (ClemMakesApps) - Added 'Download' button to the Snippets page (Justin DiPierro) - Add visibility level to project repository - Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison) - Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska) + - Fix showing commits from source project for merge request !6658 - Fix that manual jobs would no longer block jobs in the next stage. !6604 - Add configurable email subject suffix (Fu Xu) - Use defined colour for a language when available !6748 (nilsding) @@ -136,6 +174,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix buggy iOS tooltip layering behavior. - Make guests unable to view MRs on private projects - Fix broken Project API docs (Takuya Noguchi) + - Migrate invalid project members (owner -> master) ## 8.12.7 @@ -369,6 +408,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix inconsistent checkbox alignment (ClemMakesApps) - Use the default branch for displaying the project icon instead of master !5792 (Hannes Rosenögger) - Adds response mime type to transaction metric action when it's not HTML + - Fix branch protection API !6215 - Fix hover leading space bug in pipeline graph !5980 - Avoid conflict with admin labels when importing GitHub labels - User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496 diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js index f4f8cf04184..e9287d015c3 100644 --- a/app/assets/javascripts/activities.js +++ b/app/assets/javascripts/activities.js @@ -24,9 +24,7 @@ var filter = sender.attr("id").split("_")[0]; $('.event-filter .active').removeClass("active"); - $.cookie("event_filter", filter, { - path: gon.relative_url_root || '/' - }); + Cookies.set("event_filter", filter); sender.closest('li').toggleClass("active"); }; diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 8a61669822c..879f90bdc05 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -11,13 +11,13 @@ /*= require jquery-ui/effect-highlight */ /*= require jquery-ui/sortable */ /*= require jquery_ujs */ -/*= require jquery.cookie */ /*= require jquery.endless-scroll */ /*= require jquery.highlight */ /*= require jquery.waitforimages */ /*= require jquery.atwho */ /*= require jquery.scrollTo */ /*= require jquery.turbolinks */ +/*= require js.cookie */ /*= require turbolinks */ /*= require autosave */ /*= require bootstrap/affix */ @@ -83,14 +83,15 @@ }; // Disable button if text field is empty - window.disableButtonIfEmptyField = function(field_selector, button_selector) { + window.disableButtonIfEmptyField = function(field_selector, button_selector, event_name) { + event_name = event_name || 'input'; var closest_submit, field; field = $(field_selector); closest_submit = field.closest('form').find(button_selector); if (rstrip(field.val()) === "") { closest_submit.disable(); } - return field.on('input', function() { + return field.on(event_name, function() { if (rstrip($(this).val()) === "") { return closest_submit.disable(); } else { @@ -123,15 +124,11 @@ return str.replace(/<(?:.|\n)*?>/gm, ''); }; - window.unbindEvents = function() { - return $(document).off('scroll'); - }; - window.shiftWindow = function() { return scrollBy(0, -100); }; - document.addEventListener("page:fetch", unbindEvents); + document.addEventListener("page:fetch", gl.utils.cleanupBeforeFetch); window.addEventListener("hashchange", shiftWindow); @@ -148,6 +145,10 @@ $document = $(document); $window = $(window); $body = $('body'); + + // Set the default path for all cookies to GitLab's root directory + Cookies.defaults.path = gon.relative_url_root || '/'; + gl.utils.preventDisabledButtons(); bootstrapBreakpoint = bp.getBreakpointSize(); $(".nav-sidebar").niceScroll({ diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 44af1c135a0..d5966db0bf9 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -322,21 +322,18 @@ var frequentlyUsedEmojis; frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); frequentlyUsedEmojis.push(emoji); - return $.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), { - path: gon.relative_url_root || '/', - expires: 365 - }); + Cookies.set('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 }); }; AwardsHandler.prototype.getFrequentlyUsedEmojis = function() { var frequentlyUsedEmojis; - frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') || '').split(','); + frequentlyUsedEmojis = (Cookies.get('frequently_used_emojis') || '').split(','); return _.compact(_.uniq(frequentlyUsedEmojis)); }; AwardsHandler.prototype.renderFrequentlyUsedBlock = function() { var emoji, frequentlyUsedEmojis, i, len, ul; - if ($.cookie('frequently_used_emojis')) { + if (Cookies.get('frequently_used_emojis')) { frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); ul = $("
#{body}" + context = { + project: project, + pipeline: :email + } + + Banzai.render(text, context) + + context.merge!(options) + + html = Banzai.post_process(Banzai.render(text, context), context) + sanitized_html = sanitize(html, tags: HIPCHAT_ALLOWED_TAGS, attributes: %w[href title alt]) + + sanitized_html.truncate(200, separator: ' ', omission: '...') end def create_issue_message(data) @@ -134,7 +155,7 @@ class HipchatService < Service obj_attr = data[:object_attributes] obj_attr = HashWithIndifferentAccess.new(obj_attr) - title = obj_attr[:title] + title = render_line(obj_attr[:title]) state = obj_attr[:state] issue_iid = obj_attr[:iid] issue_url = obj_attr[:url] @@ -143,10 +164,7 @@ class HipchatService < Service issue_link = "issue ##{issue_iid}" message = "#{user_name} #{state} #{issue_link} in #{project_link}: #{title}" - if description - description = format_body(description) - message << description - end + message << "
#{markdown(description)}" message end @@ -159,23 +177,20 @@ class HipchatService < Service merge_request_id = obj_attr[:iid] state = obj_attr[:state] description = obj_attr[:description] - title = obj_attr[:title] + title = render_line(obj_attr[:title]) merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}" merge_request_link = "merge request !#{merge_request_id}" message = "#{user_name} #{state} #{merge_request_link} in " \ "#{project_link}: #{title}" - if description - description = format_body(description) - message << description - end + message << "
#{markdown(description)}" message end def format_title(title) - "" + title.lines.first.chomp + "" + "#{render_line(title)}" end def create_note_message(data) @@ -186,11 +201,13 @@ class HipchatService < Service note = obj_attr[:note] note_url = obj_attr[:url] noteable_type = obj_attr[:noteable_type] + commit_id = nil case noteable_type when "Commit" commit_attr = HashWithIndifferentAccess.new(data[:commit]) - subject_desc = commit_attr[:id] + commit_id = commit_attr[:id] + subject_desc = commit_id subject_desc = Commit.truncate_sha(subject_desc) subject_type = "commit" title = format_title(commit_attr[:message]) @@ -218,10 +235,7 @@ class HipchatService < Service message = "#{user_name} commented on #{subject_html} in #{project_link}: " message << title - if note - note = format_body(note) - message << note - end + message << "
#{markdown(note, ref: commit_id)}" message end diff --git a/app/models/repository.rb b/app/models/repository.rb index 72e473871fa..4ae9c20726f 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -109,6 +109,10 @@ class Repository end def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0) + unless exists? && has_visible_content? && query.present? + return [] + end + ref ||= root_ref args = %W( @@ -117,9 +121,8 @@ class Repository ) args = args.concat(%W(-- #{path})) if path.present? - git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:chomp) - commits = git_log_results.map { |c| commit(c) } - commits + git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines + git_log_results.map { |c| commit(c.chomp) }.compact end def find_branch(name, fresh_repo: true) @@ -416,6 +419,17 @@ class Repository @exists = nil end + # expire cache that doesn't depend on repository data (when expiring) + def expire_content_cache + expire_tags_cache + expire_tag_count_cache + expire_branches_cache + expire_branch_count_cache + expire_root_ref_cache + expire_emptiness_caches + expire_exists_cache + end + # Runs code after a repository has been created. def after_create expire_exists_cache @@ -431,14 +445,7 @@ class Repository expire_cache if exists? - # expire cache that don't depend on repository data (when expiring) - expire_tags_cache - expire_tag_count_cache - expire_branches_cache - expire_branch_count_cache - expire_root_ref_cache - expire_emptiness_caches - expire_exists_cache + expire_content_cache repository_event(:remove_repository) end @@ -470,14 +477,13 @@ class Repository end def before_import - expire_emptiness_caches - expire_exists_cache + expire_content_cache end # Runs code after a repository has been forked/imported. def after_import - expire_emptiness_caches - expire_exists_cache + expire_content_cache + build_cache end # Runs code after a new commit has been pushed. diff --git a/app/models/user.rb b/app/models/user.rb index f367f4616fb..9e76df63d31 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -47,7 +47,7 @@ class User < ActiveRecord::Base # # Namespace for personal projects - has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id, class_name: "Namespace" + has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id # Profile has_many :keys, dependent: :destroy @@ -66,17 +66,17 @@ class User < ActiveRecord::Base # Projects has_many :groups_projects, through: :groups, source: :projects has_many :personal_projects, through: :namespace, source: :projects - has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, class_name: 'ProjectMember' + has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy has_many :projects, through: :project_members has_many :created_projects, foreign_key: :creator_id, class_name: 'Project' has_many :users_star_projects, dependent: :destroy has_many :starred_projects, through: :users_star_projects, source: :project - has_many :snippets, dependent: :destroy, foreign_key: :author_id, class_name: "Snippet" + has_many :snippets, dependent: :destroy, foreign_key: :author_id has_many :issues, dependent: :destroy, foreign_key: :author_id has_many :notes, dependent: :destroy, foreign_key: :author_id has_many :merge_requests, dependent: :destroy, foreign_key: :author_id - has_many :events, dependent: :destroy, foreign_key: :author_id, class_name: "Event" + has_many :events, dependent: :destroy, foreign_key: :author_id has_many :subscriptions, dependent: :destroy has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event" has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue" @@ -309,7 +309,7 @@ class User < ActiveRecord::Base username end - def to_reference(_from_project = nil) + def to_reference(_from_project = nil, _target_project = nil) "#{self.class.reference_prefix}#{username}" end diff --git a/app/services/merge_requests/assign_issues_service.rb b/app/services/merge_requests/assign_issues_service.rb index f636e5fec4f..066efa1acc3 100644 --- a/app/services/merge_requests/assign_issues_service.rb +++ b/app/services/merge_requests/assign_issues_service.rb @@ -4,7 +4,7 @@ module MergeRequests @assignable_issues ||= begin if current_user == merge_request.author closes_issues.select do |issue| - !issue.assignee_id? && can?(current_user, :admin_issue, issue) + !issue.is_a?(ExternalIssue) && !issue.assignee_id? && can?(current_user, :admin_issue, issue) end else [] diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index a36008c3ef5..723cc0e6834 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -7,8 +7,10 @@ module Notes if note.award_emoji? noteable = note.noteable - todo_service.new_award_emoji(noteable, current_user) - return noteable.create_award_emoji(note.award_emoji_name, current_user) + if noteable.user_can_award?(current_user, note.award_emoji_name) + todo_service.new_award_emoji(noteable, current_user) + return noteable.create_award_emoji(note.award_emoji_name, current_user) + end end # We execute commands (extracted from `params[:note]`) on the noteable diff --git a/app/services/protected_branches/api_create_service.rb b/app/services/protected_branches/api_create_service.rb new file mode 100644 index 00000000000..f2040dfa03a --- /dev/null +++ b/app/services/protected_branches/api_create_service.rb @@ -0,0 +1,29 @@ +# The protected branches API still uses the `developers_can_push` and `developers_can_merge` +# flags for backward compatibility, and so performs translation between that format and the +# internal data model (separate access levels). The translation code is non-trivial, and so +# lives in this service. +module ProtectedBranches + class ApiCreateService < BaseService + def execute + push_access_level = + if params.delete(:developers_can_push) + Gitlab::Access::DEVELOPER + else + Gitlab::Access::MASTER + end + + merge_access_level = + if params.delete(:developers_can_merge) + Gitlab::Access::DEVELOPER + else + Gitlab::Access::MASTER + end + + @params.merge!(push_access_levels_attributes: [{ access_level: push_access_level }], + merge_access_levels_attributes: [{ access_level: merge_access_level }]) + + service = ProtectedBranches::CreateService.new(@project, @current_user, @params) + service.execute + end + end +end diff --git a/app/services/protected_branches/api_update_service.rb b/app/services/protected_branches/api_update_service.rb new file mode 100644 index 00000000000..050cb3b738b --- /dev/null +++ b/app/services/protected_branches/api_update_service.rb @@ -0,0 +1,47 @@ +# The protected branches API still uses the `developers_can_push` and `developers_can_merge` +# flags for backward compatibility, and so performs translation between that format and the +# internal data model (separate access levels). The translation code is non-trivial, and so +# lives in this service. +module ProtectedBranches + class ApiUpdateService < BaseService + def execute(protected_branch) + @developers_can_push = params.delete(:developers_can_push) + @developers_can_merge = params.delete(:developers_can_merge) + + @protected_branch = protected_branch + + protected_branch.transaction do + delete_redundant_access_levels + + case @developers_can_push + when true + params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }]) + when false + params.merge!(push_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }]) + end + + case @developers_can_merge + when true + params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::DEVELOPER }]) + when false + params.merge!(merge_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }]) + end + + service = ProtectedBranches::UpdateService.new(@project, @current_user, @params) + service.execute(protected_branch) + end + end + + private + + def delete_redundant_access_levels + unless @developers_can_merge.nil? + @protected_branch.merge_access_levels.destroy_all + end + + unless @developers_can_push.nil? + @protected_branch.push_access_levels.destroy_all + end + end + end +end diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index b760b42fde0..37bb6a3b0e0 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -75,4 +75,4 @@ - @runners.each do |runner| = render "admin/runners/runner", runner: runner - = paginate @runners + = paginate @runners, theme: "gitlab" diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 10fea1996aa..73038164056 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -67,7 +67,7 @@ = form_for [:admin, project.namespace.becomes(Namespace), project, project.runner_projects.new] do |f| = f.hidden_field :runner_id, value: @runner.id = f.submit 'Enable', class: 'btn btn-xs' - = paginate @projects + = paginate @projects, theme: "gitlab" .col-md-6 %h4 Recent builds served by this Runner diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index a96b579c593..5fd896f6835 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -5,12 +5,12 @@ %div.form-group = f.label :password = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required." + %div.submit-container.move-submit-down + = f.submit "Sign in", class: "btn btn-save" - if devise_mapping.rememberable? .remember-me.checkbox %label{for: "user_remember_me"} = f.check_box :remember_me %span Remember me - .pull-right + .pull-right.forgot-password = link_to "Forgot your password?", new_password_path(resource_name) - %div.submit-container - = f.submit "Sign in", class: "btn btn-save" diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index 0e865b807c1..fd77cdbee2e 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -10,7 +10,7 @@ = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: 'edit_user show-gl-field-errors' }) do |f| - resource_params = params[resource_name].presence || params = f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0) - .form-group + %div = f.label 'Two-Factor Authentication code', name: :otp_attempt = f.text_field :otp_attempt, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: 'This field is required.' %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml index a057f126c45..1e957f0935f 100644 --- a/app/views/devise/shared/_tabs_ldap.html.haml +++ b/app/views/devise/shared/_tabs_ldap.html.haml @@ -1,4 +1,4 @@ -%ul.new-session-tabs.nav-links.nav-tabs +%ul.new-session-tabs.nav-links.nav-tabs{ class: ('custom-provider-tabs' if form_based_providers.any?) } - if crowd_enabled? %li.active = link_to "Crowd", "#crowd", 'data-toggle' => 'tab' diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml index 79b1d447a92..05246303fb6 100644 --- a/app/views/devise/shared/_tabs_normal.html.haml +++ b/app/views/devise/shared/_tabs_normal.html.haml @@ -1,5 +1,6 @@ %ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist'} %li.active{ role: 'presentation' } %a{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab'} Sign in - %li{ role: 'presentation'} - %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab'} Register + - if signin_enabled? && signup_enabled? + %li{ role: 'presentation'} + %a{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab'} Register diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml index 44e2653ca4a..767dffb5589 100644 --- a/app/views/import/gitlab_projects/new.html.haml +++ b/app/views/import/gitlab_projects/new.html.haml @@ -9,12 +9,12 @@ %p Project will be imported as %strong - #{@namespace_name}/#{@path} + #{@namespace.name}/#{@path} %p To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here. .form-group - = hidden_field_tag :namespace_id, @namespace_id + = hidden_field_tag :namespace_id, @namespace.id = hidden_field_tag :path, @path = label_tag :file, class: 'control-label' do %span GitLab project export diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml index 22c1dfa84fc..8fce702314c 100644 --- a/app/views/projects/boards/components/_card.html.haml +++ b/app/views/projects/boards/components/_card.html.haml @@ -21,7 +21,7 @@ %span.card-number{ "v-if" => "issue.id" } = precede '#' do {{ issue.id }} - %a.has-tooltip{ ":href" => "'/' + issue.assignee.username", + %a.has-tooltip{ ":href" => "'#{root_path}' + issue.assignee.username", ":title" => "'Assigned to ' + issue.assignee.name", "v-if" => "issue.assignee", data: { container: 'body' } } diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml index 017d3ff6af2..55965172d3f 100644 --- a/app/views/projects/ci/builds/_build_pipeline.html.haml +++ b/app/views/projects/ci/builds/_build_pipeline.html.haml @@ -1,10 +1,10 @@ - is_playable = subject.playable? && can?(current_user, :update_build, @project) - if is_playable - = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: 'Play' do + = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.pipeline-graph', placement: 'bottom' } do = render_status_with_link('build', 'play') .ci-status-text= subject.name - elsif can?(current_user, :read_build, @project) - = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do + = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } do %span.ci-status-icon = render_status_with_link('build', subject.status) .ci-status-text= subject.name diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml index 5d0d5ba0262..f2d71fa6989 100644 --- a/app/views/projects/commit/_pipeline_status_group.html.haml +++ b/app/views/projects/commit/_pipeline_status_group.html.haml @@ -1,5 +1,5 @@ - group_status = CommitStatus.where(id: subject).status -%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } } +%button.dropdown-menu-toggle.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } } %span.ci-status-icon = render_status_with_link('build', group_status) %span.ci-status-text diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index 76b68c544aa..7bde20c3286 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -10,7 +10,7 @@ = button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do .dropdown-toggle-text= params[:from] || 'Select branch/tag' = render "ref_dropdown" - .compare-ellipsis ... + .compare-ellipsis.inline ... .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown .input-group.inline-input-group %span.input-group-addon to diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index fb776e3a3e7..30473d14b9b 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -46,70 +46,70 @@ %h5.prepend-top-0 Feature Visibility - = f.fields_for :project_feature do |feature_fields| - .form_group.prepend-top-20 - .row - .col-md-9 - = feature_fields.label :repository_access_level, "Repository", class: 'label-light' - %span.help-block Push files to be stored in this project - .col-md-3.js-repo-access-level - = project_feature_access_select(:repository_access_level) + = f.fields_for :project_feature do |feature_fields| + .form_group.prepend-top-20 + .row + .col-md-9 + = feature_fields.label :repository_access_level, "Repository", class: 'label-light' + %span.help-block Push files to be stored in this project + .col-md-3.js-repo-access-level + = project_feature_access_select(:repository_access_level) - .col-sm-12 - .row - .col-md-9.project-feature-nested - = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light' - %span.help-block Submit changes to be merged upstream - .col-md-3 - = project_feature_access_select(:merge_requests_access_level) + .col-sm-12 + .row + .col-md-9.project-feature-nested + = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light' + %span.help-block Submit changes to be merged upstream + .col-md-3 + = project_feature_access_select(:merge_requests_access_level) - .row - .col-md-9.project-feature-nested - = feature_fields.label :builds_access_level, "Builds", class: 'label-light' - %span.help-block Submit, test and deploy your changes before merge - .col-md-3 - = project_feature_access_select(:builds_access_level) + .row + .col-md-9.project-feature-nested + = feature_fields.label :builds_access_level, "Builds", class: 'label-light' + %span.help-block Submit, test and deploy your changes before merge + .col-md-3 + = project_feature_access_select(:builds_access_level) - .row - .col-md-9 - = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light' - %span.help-block Share code pastes with others out of Git repository - .col-md-3 - = project_feature_access_select(:snippets_access_level) + .row + .col-md-9 + = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light' + %span.help-block Share code pastes with others out of Git repository + .col-md-3 + = project_feature_access_select(:snippets_access_level) - .row - .col-md-9 - = feature_fields.label :issues_access_level, "Issues", class: 'label-light' - %span.help-block Lightweight issue tracking system for this project - .col-md-3 - = project_feature_access_select(:issues_access_level) + .row + .col-md-9 + = feature_fields.label :issues_access_level, "Issues", class: 'label-light' + %span.help-block Lightweight issue tracking system for this project + .col-md-3 + = project_feature_access_select(:issues_access_level) - .row - .col-md-9 - = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light' - %span.help-block Pages for project documentation - .col-md-3 - = project_feature_access_select(:wiki_access_level) - - - if Gitlab.config.lfs.enabled && current_user.admin? - .checkbox - = f.label :lfs_enabled do - = f.check_box :lfs_enabled - %strong LFS - %br - %span.descr - Git Large File Storage - = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + .row + .col-md-9 + = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light' + %span.help-block Pages for project documentation + .col-md-3 + = project_feature_access_select(:wiki_access_level) - if Gitlab.config.lfs.enabled && current_user.admin? - .form-group - .checkbox - = f.label :container_registry_enabled do - = f.check_box :container_registry_enabled - %strong Container Registry - %br - %span.descr Enable Container Registry for this project - = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank' + .checkbox + = f.label :lfs_enabled do + = f.check_box :lfs_enabled + %strong LFS + %br + %span.descr + Git Large File Storage + = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + + - if Gitlab.config.registry.enabled + .form-group + .checkbox + = f.label :container_registry_enabled do + = f.check_box :container_registry_enabled + %strong Container Registry + %br + %span.descr Enable Container Registry for this project + = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank' = render 'merge_request_settings', f: f %hr @@ -288,4 +288,4 @@ Saving project. %p Please wait a moment, this page will automatically refresh when ready. -= render 'shared/confirm_modal', phrase: @project.path += render 'shared/confirm_modal', phrase: @project.path \ No newline at end of file diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml index 0a66d60accc..c45b73e4225 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml @@ -1,9 +1,10 @@ -- if subject.target_url - = link_to subject.target_url do +%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } } + - if subject.target_url + = link_to subject.target_url do + %span.ci-status-icon + = render_status_with_link('commit status', subject.status) + %span.ci-status-text= subject.name + - else %span.ci-status-icon = render_status_with_link('commit status', subject.status) %span.ci-status-text= subject.name -- else - %span.ci-status-icon - = render_status_with_link('commit status', subject.status) - %span.ci-status-text= subject.name diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 6f3f238a436..bd629b5c519 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -53,7 +53,7 @@ .issue-details.issuable-details - .detail-page-description.content-block + .detail-page-description.content-block{ class: ('hide-bottom-border' unless @issue.description.present? ) } %h2.title = markdown_field(@issue, :title) - if @issue.description.present? diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index cd98aaf8d75..f57abe73977 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -26,19 +26,19 @@ %ul.dropdown-menu.dropdown-menu-align-right %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) - - unless @merge_request.closed_without_fork? - .normal - %span Request to merge - %span.label-branch= source_branch_with_namespace(@merge_request) - %span into - %span.label-branch - = link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch) - - if @merge_request.open? && @merge_request.diverged_from_target_branch? - %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind) + .normal + %span Request to merge + %span.label-branch= source_branch_with_namespace(@merge_request) + %span into + %span.label-branch + = link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch) + - if @merge_request.open? && @merge_request.diverged_from_target_branch? + %span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind) - - unless @merge_request.closed_without_source_project? + - if @merge_request.source_branch_exists? = render "projects/merge_requests/show/how_to_merge" - = render "projects/merge_requests/widget/show.html.haml" + + = render "projects/merge_requests/widget/show.html.haml" - if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user) .light.prepend-top-default.append-bottom-default @@ -47,39 +47,41 @@ = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" - if @commits_count.nonzero? - %ul.merge-request-tabs.nav-links.no-top.no-bottom{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } - %li.notes-tab - = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do - Discussion - %span.badge= @merge_request.mr_and_commit_notes.user.count - - unless @merge_request.closed_without_source_project? - %li.commits-tab - = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do - Commits - %span.badge= @commits_count - - if @pipeline - %li.pipelines-tab - = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do - Pipelines - %span.badge= @pipelines.size - %li.builds-tab - = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do - Builds - %span.badge= @statuses.size - %li.diffs-tab - = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do - Changes - %span.badge= @merge_request.diff_size - %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true } - %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" } - .line-resolve-all{ "v-show" => "discussionCount > 0", - ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" } - %span.line-resolve-btn.is-disabled{ type: "button", - ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" } - = render "shared/icons/icon_status_success.svg" - %span.line-resolve-text - {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ discussionCount | pluralize 'discussion' }} resolved - = render "discussions/jump_to_next" + .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } + %div{ class: container_class } + %ul.merge-request-tabs.nav-links.no-top.no-bottom + %li.notes-tab + = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do + Discussion + %span.badge= @merge_request.mr_and_commit_notes.user.count + - if @merge_request.source_project + %li.commits-tab + = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do + Commits + %span.badge= @commits_count + - if @pipeline + %li.pipelines-tab + = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do + Pipelines + %span.badge= @pipelines.size + %li.builds-tab + = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do + Builds + %span.badge= @statuses.size + %li.diffs-tab + = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do + Changes + %span.badge= @merge_request.diff_size + %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true } + %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" } + .line-resolve-all{ "v-show" => "discussionCount > 0", + ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" } + %span.line-resolve-btn.is-disabled{ type: "button", + ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" } + = render "shared/icons/icon_status_success.svg" + %span.line-resolve-text + {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ discussionCount | pluralize 'discussion' }} resolved + = render "discussions/jump_to_next" .tab-content#diff-notes-app #notes.notes.tab-pane.voting_notes diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml index 0b05785430b..61020516bcf 100644 --- a/app/views/projects/merge_requests/show/_commits.html.haml +++ b/app/views/projects/merge_requests/show/_commits.html.haml @@ -3,4 +3,4 @@ Most recent commits displayed first %ol#commits-list.list-unstyled - = render "projects/commits/commits", project: @merge_request.project + = render "projects/commits/commits", project: @merge_request.source_project diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index c83818e9199..f9ba77e87b5 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -31,7 +31,7 @@ = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do Delete - .detail-page-description.milestone-detail + .detail-page-description.milestone-detail{ class: ('hide-bottom-border' unless @milestone.description.present? ) } %h2.title = markdown_field(@milestone, :title) %div diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index cc8cb134fb8..932603f03b0 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -27,6 +27,7 @@ - else .input-group-addon.static-namespace #{root_url}#{current_user.username}/ + = f.hidden_field :namespace_id, value: current_user.namespace_id .form-group.col-xs-12.col-sm-6.project-path = f.label :namespace_id, class: 'label-light' do %span @@ -126,6 +127,11 @@ } }); + $('#new_project').submit(function(){ + var $path = $('#project_path'); + $path.val($path.val().trim()); + }); + $('#project_path').keyup(function(){ if($(this).val().length !=0) { $('.btn_import_gitlab_project').attr('disabled', false); diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 73fe6a715fa..ab719e38904 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -57,7 +57,7 @@ = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do = icon('pencil', class: 'link-highlight') = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do - = icon('trash-o') + = icon('trash-o', class: 'danger-highlight') .note-body{class: note_editable ? 'js-task-list-container' : ''} .note-text.md = preserve do diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 49dcc9a6ba4..42e9bdbd30e 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -1,4 +1,6 @@ - page_title "Protected branches" +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('protected_branches/protected_branches_bundle.js') .row.prepend-top-default.append-bottom-default .col-lg-3 diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml index 858af78f7bf..51b0939564e 100644 --- a/app/views/projects/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -26,4 +26,4 @@ %h4.underlined-title Available specific runners %ul.bordered-list.available-specific-runners = render partial: 'runner', collection: @assignable_runners, as: :runner - = paginate @assignable_runners + = paginate @assignable_runners, theme: "gitlab" diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb index 667fff031dd..c2dc955b27c 100644 --- a/app/workers/admin_email_worker.rb +++ b/app/workers/admin_email_worker.rb @@ -1,7 +1,6 @@ class AdminEmailWorker include Sidekiq::Worker - - sidekiq_options retry: false # this job auto-repeats via sidekiq-cron + include CronjobQueue def perform repository_check_failed_count = Project.where(last_repository_check_failed: true).count diff --git a/app/workers/build_coverage_worker.rb b/app/workers/build_coverage_worker.rb index 0680645a8db..def0ab1dde1 100644 --- a/app/workers/build_coverage_worker.rb +++ b/app/workers/build_coverage_worker.rb @@ -1,6 +1,6 @@ class BuildCoverageWorker include Sidekiq::Worker - sidekiq_options queue: :default + include BuildQueue def perform(build_id) Ci::Build.find_by(id: build_id) diff --git a/app/workers/build_email_worker.rb b/app/workers/build_email_worker.rb index 1c7a04a66a8..5fdb1f2baa0 100644 --- a/app/workers/build_email_worker.rb +++ b/app/workers/build_email_worker.rb @@ -1,5 +1,6 @@ class BuildEmailWorker include Sidekiq::Worker + include BuildQueue def perform(build_id, recipients, push_data) recipients.each do |recipient| diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb index e7286b77ac5..466410bf08c 100644 --- a/app/workers/build_finished_worker.rb +++ b/app/workers/build_finished_worker.rb @@ -1,5 +1,6 @@ class BuildFinishedWorker include Sidekiq::Worker + include BuildQueue def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb index e22ececb3fd..9965af935d4 100644 --- a/app/workers/build_hooks_worker.rb +++ b/app/workers/build_hooks_worker.rb @@ -1,6 +1,6 @@ class BuildHooksWorker include Sidekiq::Worker - sidekiq_options queue: :default + include BuildQueue def perform(build_id) Ci::Build.find_by(id: build_id) diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb index 500d357ce31..e0ad5268664 100644 --- a/app/workers/build_success_worker.rb +++ b/app/workers/build_success_worker.rb @@ -1,6 +1,6 @@ class BuildSuccessWorker include Sidekiq::Worker - sidekiq_options queue: :default + include BuildQueue def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| diff --git a/app/workers/clear_database_cache_worker.rb b/app/workers/clear_database_cache_worker.rb index c541daba50e..c4cb4733482 100644 --- a/app/workers/clear_database_cache_worker.rb +++ b/app/workers/clear_database_cache_worker.rb @@ -1,6 +1,7 @@ # This worker clears all cache fields in the database, working in batches. class ClearDatabaseCacheWorker include Sidekiq::Worker + include DedicatedSidekiqQueue BATCH_SIZE = 1000 diff --git a/app/workers/concerns/build_queue.rb b/app/workers/concerns/build_queue.rb new file mode 100644 index 00000000000..cf0ead40a8b --- /dev/null +++ b/app/workers/concerns/build_queue.rb @@ -0,0 +1,8 @@ +# Concern for setting Sidekiq settings for the various CI build workers. +module BuildQueue + extend ActiveSupport::Concern + + included do + sidekiq_options queue: :build + end +end diff --git a/app/workers/concerns/cronjob_queue.rb b/app/workers/concerns/cronjob_queue.rb new file mode 100644 index 00000000000..e918bb011e0 --- /dev/null +++ b/app/workers/concerns/cronjob_queue.rb @@ -0,0 +1,9 @@ +# Concern that sets various Sidekiq settings for workers executed using a +# cronjob. +module CronjobQueue + extend ActiveSupport::Concern + + included do + sidekiq_options queue: :cronjob, retry: false + end +end diff --git a/app/workers/concerns/dedicated_sidekiq_queue.rb b/app/workers/concerns/dedicated_sidekiq_queue.rb new file mode 100644 index 00000000000..132bae6022b --- /dev/null +++ b/app/workers/concerns/dedicated_sidekiq_queue.rb @@ -0,0 +1,9 @@ +# Concern that sets the queue of a Sidekiq worker based on the worker's class +# name/namespace. +module DedicatedSidekiqQueue + extend ActiveSupport::Concern + + included do + sidekiq_options queue: name.sub(/Worker\z/, '').underscore.tr('/', '_') + end +end diff --git a/app/workers/concerns/pipeline_queue.rb b/app/workers/concerns/pipeline_queue.rb new file mode 100644 index 00000000000..ca3860e1d38 --- /dev/null +++ b/app/workers/concerns/pipeline_queue.rb @@ -0,0 +1,8 @@ +# Concern for setting Sidekiq settings for the various CI pipeline workers. +module PipelineQueue + extend ActiveSupport::Concern + + included do + sidekiq_options queue: :pipeline + end +end diff --git a/app/workers/concerns/repository_check_queue.rb b/app/workers/concerns/repository_check_queue.rb new file mode 100644 index 00000000000..a597321ccf4 --- /dev/null +++ b/app/workers/concerns/repository_check_queue.rb @@ -0,0 +1,8 @@ +# Concern for setting Sidekiq settings for the various repository check workers. +module RepositoryCheckQueue + extend ActiveSupport::Concern + + included do + sidekiq_options queue: :repository_check, retry: false + end +end diff --git a/app/workers/delete_user_worker.rb b/app/workers/delete_user_worker.rb index 6ff361e4d80..3194c389b3d 100644 --- a/app/workers/delete_user_worker.rb +++ b/app/workers/delete_user_worker.rb @@ -1,5 +1,6 @@ class DeleteUserWorker include Sidekiq::Worker + include DedicatedSidekiqQueue def perform(current_user_id, delete_user_id, options = {}) delete_user = User.find(delete_user_id) diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb index 842eebdea9e..d3f7e479a8d 100644 --- a/app/workers/email_receiver_worker.rb +++ b/app/workers/email_receiver_worker.rb @@ -1,7 +1,6 @@ class EmailReceiverWorker include Sidekiq::Worker - - sidekiq_options queue: :incoming_email + include DedicatedSidekiqQueue def perform(raw) return unless Gitlab::IncomingEmail.enabled? diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index 1dc7e0adef7..b9cd49985dc 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -1,7 +1,7 @@ class EmailsOnPushWorker include Sidekiq::Worker + include DedicatedSidekiqQueue - sidekiq_options queue: :mailers attr_reader :email, :skip_premailer def perform(project_id, recipients, push_data, options = {}) diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb index 174eabff9fd..a27585fd389 100644 --- a/app/workers/expire_build_artifacts_worker.rb +++ b/app/workers/expire_build_artifacts_worker.rb @@ -1,5 +1,6 @@ class ExpireBuildArtifactsWorker include Sidekiq::Worker + include CronjobQueue def perform Rails.logger.info 'Scheduling removal of build artifacts' diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb index d9e2cc37bb3..eb403c134d1 100644 --- a/app/workers/expire_build_instance_artifacts_worker.rb +++ b/app/workers/expire_build_instance_artifacts_worker.rb @@ -1,5 +1,6 @@ class ExpireBuildInstanceArtifactsWorker include Sidekiq::Worker + include DedicatedSidekiqQueue def perform(build_id) build = Ci::Build diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb index a6cefd4d601..65f8093b5b0 100644 --- a/app/workers/git_garbage_collect_worker.rb +++ b/app/workers/git_garbage_collect_worker.rb @@ -1,8 +1,9 @@ class GitGarbageCollectWorker include Sidekiq::Worker include Gitlab::ShellAdapter + include DedicatedSidekiqQueue - sidekiq_options queue: :gitlab_shell, retry: false + sidekiq_options retry: false def perform(project_id) project = Project.find(project_id) diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb index cfeda88bbc5..964287a1793 100644 --- a/app/workers/gitlab_shell_worker.rb +++ b/app/workers/gitlab_shell_worker.rb @@ -1,8 +1,7 @@ class GitlabShellWorker include Sidekiq::Worker include Gitlab::ShellAdapter - - sidekiq_options queue: :gitlab_shell + include DedicatedSidekiqQueue def perform(action, *arg) gitlab_shell.send(action, *arg) diff --git a/app/workers/group_destroy_worker.rb b/app/workers/group_destroy_worker.rb index 5048746f09b..a49a5fd0855 100644 --- a/app/workers/group_destroy_worker.rb +++ b/app/workers/group_destroy_worker.rb @@ -1,7 +1,6 @@ class GroupDestroyWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include DedicatedSidekiqQueue def perform(group_id, user_id) begin diff --git a/app/workers/import_export_project_cleanup_worker.rb b/app/workers/import_export_project_cleanup_worker.rb index 72e3a9ae734..7957ed807ab 100644 --- a/app/workers/import_export_project_cleanup_worker.rb +++ b/app/workers/import_export_project_cleanup_worker.rb @@ -1,7 +1,6 @@ class ImportExportProjectCleanupWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include CronjobQueue def perform ImportExportCleanUpService.new.execute diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb index 19f38358eb5..7e44b241743 100644 --- a/app/workers/irker_worker.rb +++ b/app/workers/irker_worker.rb @@ -3,6 +3,7 @@ require 'socket' class IrkerWorker include Sidekiq::Worker + include DedicatedSidekiqQueue def perform(project_id, chans, colors, push_data, settings) project = Project.find(project_id) diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb index c87c0a252b1..79efca4f2f9 100644 --- a/app/workers/merge_worker.rb +++ b/app/workers/merge_worker.rb @@ -1,7 +1,6 @@ class MergeWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include DedicatedSidekiqQueue def perform(merge_request_id, current_user_id, params) params = params.with_indifferent_access diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb index 1b3232cd365..c3e62bb88c0 100644 --- a/app/workers/new_note_worker.rb +++ b/app/workers/new_note_worker.rb @@ -1,7 +1,6 @@ class NewNoteWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include DedicatedSidekiqQueue def perform(note_id, note_params) note = Note.find(note_id) diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb index ab5e9f6daad..7e36eacebf8 100644 --- a/app/workers/pipeline_hooks_worker.rb +++ b/app/workers/pipeline_hooks_worker.rb @@ -1,6 +1,6 @@ class PipelineHooksWorker include Sidekiq::Worker - sidekiq_options queue: :default + include PipelineQueue def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id) diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb index 7bb92df3bbd..34f6ef161fb 100644 --- a/app/workers/pipeline_metrics_worker.rb +++ b/app/workers/pipeline_metrics_worker.rb @@ -1,7 +1,6 @@ class PipelineMetricsWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include PipelineQueue def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline| diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb index f44227d7086..357e4a9a1c3 100644 --- a/app/workers/pipeline_process_worker.rb +++ b/app/workers/pipeline_process_worker.rb @@ -1,7 +1,6 @@ class PipelineProcessWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include PipelineQueue def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id) diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb index 5dd443fea59..2aa6fff24da 100644 --- a/app/workers/pipeline_success_worker.rb +++ b/app/workers/pipeline_success_worker.rb @@ -1,6 +1,6 @@ class PipelineSuccessWorker include Sidekiq::Worker - sidekiq_options queue: :default + include PipelineQueue def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline| diff --git a/app/workers/pipeline_update_worker.rb b/app/workers/pipeline_update_worker.rb index 44a7f24e401..96c4152c674 100644 --- a/app/workers/pipeline_update_worker.rb +++ b/app/workers/pipeline_update_worker.rb @@ -1,7 +1,6 @@ class PipelineUpdateWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include PipelineQueue def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id) diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index a9a2b716005..eee0ca12af9 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -1,7 +1,6 @@ class PostReceive include Sidekiq::Worker - - sidekiq_options queue: :post_receive + include DedicatedSidekiqQueue def perform(repo_path, identifier, changes) if path = Gitlab.config.repositories.storages.find { |p| repo_path.start_with?(p[1].to_s) } diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb index ccefd0f71a0..4dfa745fb50 100644 --- a/app/workers/project_cache_worker.rb +++ b/app/workers/project_cache_worker.rb @@ -1,9 +1,41 @@ +# Worker for updating any project specific caches. +# +# This worker runs at most once every 15 minutes per project. This is to ensure +# that multiple instances of jobs for this worker don't hammer the underlying +# storage engine as much. class ProjectCacheWorker include Sidekiq::Worker + include DedicatedSidekiqQueue - sidekiq_options queue: :default + LEASE_TIMEOUT = 15.minutes.to_i + + def self.lease_for(project_id) + Gitlab::ExclusiveLease. + new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT) + end + + # Overwrite Sidekiq's implementation so we only schedule when actually needed. + def self.perform_async(project_id) + # If a lease for this project is still being held there's no point in + # scheduling a new job. + super unless lease_for(project_id).exists? + end def perform(project_id) + if try_obtain_lease_for(project_id) + Rails.logger. + info("Obtained ProjectCacheWorker lease for project #{project_id}") + else + Rails.logger. + info("Could not obtain ProjectCacheWorker lease for project #{project_id}") + + return + end + + update_caches(project_id) + end + + def update_caches(project_id) project = Project.find(project_id) return unless project.repository.exists? @@ -15,4 +47,8 @@ class ProjectCacheWorker project.repository.build_cache end end + + def try_obtain_lease_for(project_id) + self.class.lease_for(project_id).try_obtain + end end diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb index 3062301a9b1..b462327490e 100644 --- a/app/workers/project_destroy_worker.rb +++ b/app/workers/project_destroy_worker.rb @@ -1,7 +1,6 @@ class ProjectDestroyWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include DedicatedSidekiqQueue def perform(project_id, user_id, params) begin diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb index 615311e63f5..6009aa1b191 100644 --- a/app/workers/project_export_worker.rb +++ b/app/workers/project_export_worker.rb @@ -1,7 +1,8 @@ class ProjectExportWorker include Sidekiq::Worker + include DedicatedSidekiqQueue - sidekiq_options queue: :gitlab_shell, retry: 3 + sidekiq_options retry: 3 def perform(current_user_id, project_id) current_user = User.find(current_user_id) diff --git a/app/workers/project_service_worker.rb b/app/workers/project_service_worker.rb index 64d39c4d3f7..fdfdeab7b41 100644 --- a/app/workers/project_service_worker.rb +++ b/app/workers/project_service_worker.rb @@ -1,7 +1,6 @@ class ProjectServiceWorker include Sidekiq::Worker - - sidekiq_options queue: :project_web_hook + include DedicatedSidekiqQueue def perform(hook_id, data) data = data.with_indifferent_access diff --git a/app/workers/project_web_hook_worker.rb b/app/workers/project_web_hook_worker.rb index fb878965288..efb85eafd15 100644 --- a/app/workers/project_web_hook_worker.rb +++ b/app/workers/project_web_hook_worker.rb @@ -1,7 +1,6 @@ class ProjectWebHookWorker include Sidekiq::Worker - - sidekiq_options queue: :project_web_hook + include DedicatedSidekiqQueue def perform(hook_id, data, hook_name) data = data.with_indifferent_access diff --git a/app/workers/prune_old_events_worker.rb b/app/workers/prune_old_events_worker.rb index 5883cafe1d1..392abb9c21b 100644 --- a/app/workers/prune_old_events_worker.rb +++ b/app/workers/prune_old_events_worker.rb @@ -1,5 +1,6 @@ class PruneOldEventsWorker include Sidekiq::Worker + include CronjobQueue def perform # Contribution calendar shows maximum 12 months of events. diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb index 246c8b6650a..2a619f83410 100644 --- a/app/workers/remove_expired_group_links_worker.rb +++ b/app/workers/remove_expired_group_links_worker.rb @@ -1,5 +1,6 @@ class RemoveExpiredGroupLinksWorker include Sidekiq::Worker + include CronjobQueue def perform ProjectGroupLink.expired.destroy_all diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb index cf765af97ce..31f652e5f9b 100644 --- a/app/workers/remove_expired_members_worker.rb +++ b/app/workers/remove_expired_members_worker.rb @@ -1,5 +1,6 @@ class RemoveExpiredMembersWorker include Sidekiq::Worker + include CronjobQueue def perform Member.expired.find_each do |member| diff --git a/app/workers/repository_archive_cache_worker.rb b/app/workers/repository_archive_cache_worker.rb index a2e49c61f59..e47069df189 100644 --- a/app/workers/repository_archive_cache_worker.rb +++ b/app/workers/repository_archive_cache_worker.rb @@ -1,7 +1,6 @@ class RepositoryArchiveCacheWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include CronjobQueue def perform RepositoryArchiveCleanUpService.new.execute diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb index a3e16fa5212..c3e7491ec4e 100644 --- a/app/workers/repository_check/batch_worker.rb +++ b/app/workers/repository_check/batch_worker.rb @@ -1,14 +1,13 @@ module RepositoryCheck class BatchWorker include Sidekiq::Worker - + include CronjobQueue + RUN_TIME = 3600 - - sidekiq_options retry: false - + def perform start = Time.now - + # This loop will break after a little more than one hour ('a little # more' because `git fsck` may take a few minutes), or if it runs out of # projects to check. By default sidekiq-cron will start a new @@ -17,15 +16,15 @@ module RepositoryCheck project_ids.each do |project_id| break if Time.now - start >= RUN_TIME break unless current_settings.repository_checks_enabled - + next unless try_obtain_lease(project_id) - + SingleRepositoryWorker.new.perform(project_id) end end - + private - + # Project.find_each does not support WHERE clauses and # Project.find_in_batches does not support ordering. So we just build an # array of ID's. This is OK because we do it only once an hour, because @@ -39,7 +38,7 @@ module RepositoryCheck reorder('last_repository_check_at ASC').limit(limit).pluck(:id) never_checked_projects + old_check_projects end - + def try_obtain_lease(id) # Use a 24-hour timeout because on servers/projects where 'git fsck' is # super slow we definitely do not want to run it twice in parallel. @@ -48,7 +47,7 @@ module RepositoryCheck timeout: 24.hours ).try_obtain end - + def current_settings # No caching of the settings! If we cache them and an admin disables # this feature, an active RepositoryCheckWorker would keep going for up diff --git a/app/workers/repository_check/clear_worker.rb b/app/workers/repository_check/clear_worker.rb index b7202ddff34..1f1b38540ee 100644 --- a/app/workers/repository_check/clear_worker.rb +++ b/app/workers/repository_check/clear_worker.rb @@ -1,8 +1,7 @@ module RepositoryCheck class ClearWorker include Sidekiq::Worker - - sidekiq_options retry: false + include RepositoryCheckQueue def perform # Do small batched updates because these updates will be slow and locking diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb index 98ddf5d0688..3d8bfc6fc6c 100644 --- a/app/workers/repository_check/single_repository_worker.rb +++ b/app/workers/repository_check/single_repository_worker.rb @@ -1,8 +1,7 @@ module RepositoryCheck class SingleRepositoryWorker include Sidekiq::Worker - - sidekiq_options retry: false + include RepositoryCheckQueue def perform(project_id) project = Project.find(project_id) diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index 61ed1c38ac4..efc99ec962a 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -1,8 +1,7 @@ class RepositoryForkWorker include Sidekiq::Worker include Gitlab::ShellAdapter - - sidekiq_options queue: :gitlab_shell + include DedicatedSidekiqQueue def perform(project_id, forked_from_repository_storage_path, source_path, target_path) Gitlab::Metrics.add_event(:fork_repository, diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index d2ca8813ab9..c8a77e21c12 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -1,8 +1,7 @@ class RepositoryImportWorker include Sidekiq::Worker include Gitlab::ShellAdapter - - sidekiq_options queue: :gitlab_shell + include DedicatedSidekiqQueue attr_accessor :project, :current_user diff --git a/app/workers/requests_profiles_worker.rb b/app/workers/requests_profiles_worker.rb index 9dd228a2483..703b025d76e 100644 --- a/app/workers/requests_profiles_worker.rb +++ b/app/workers/requests_profiles_worker.rb @@ -1,7 +1,6 @@ class RequestsProfilesWorker include Sidekiq::Worker - - sidekiq_options queue: :default + include CronjobQueue def perform Gitlab::RequestProfiler.remove_all_profiles diff --git a/app/workers/stuck_ci_builds_worker.rb b/app/workers/stuck_ci_builds_worker.rb index 6828013b377..b70df5a1afa 100644 --- a/app/workers/stuck_ci_builds_worker.rb +++ b/app/workers/stuck_ci_builds_worker.rb @@ -1,5 +1,6 @@ class StuckCiBuildsWorker include Sidekiq::Worker + include CronjobQueue BUILD_STUCK_TIMEOUT = 1.day diff --git a/app/workers/system_hook_worker.rb b/app/workers/system_hook_worker.rb index a122c274763..baf2f12eeac 100644 --- a/app/workers/system_hook_worker.rb +++ b/app/workers/system_hook_worker.rb @@ -1,7 +1,6 @@ class SystemHookWorker include Sidekiq::Worker - - sidekiq_options queue: :system_hook + include DedicatedSidekiqQueue def perform(hook_id, data, hook_name) SystemHook.find(hook_id).execute(data, hook_name) diff --git a/app/workers/trending_projects_worker.rb b/app/workers/trending_projects_worker.rb index df4c4a6628b..0531630d13a 100644 --- a/app/workers/trending_projects_worker.rb +++ b/app/workers/trending_projects_worker.rb @@ -1,7 +1,6 @@ class TrendingProjectsWorker include Sidekiq::Worker - - sidekiq_options queue: :trending_projects + include CronjobQueue def perform Rails.logger.info('Refreshing trending projects') diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb index 03f0528cdae..acc4d858136 100644 --- a/app/workers/update_merge_requests_worker.rb +++ b/app/workers/update_merge_requests_worker.rb @@ -1,5 +1,6 @@ class UpdateMergeRequestsWorker include Sidekiq::Worker + include DedicatedSidekiqQueue def perform(project_id, user_id, oldrev, newrev, ref) project = Project.find_by(id: project_id) diff --git a/bin/background_jobs b/bin/background_jobs index 25a578a1c49..f28e2f722dc 100755 --- a/bin/background_jobs +++ b/bin/background_jobs @@ -4,6 +4,7 @@ cd $(dirname $0)/.. app_root=$(pwd) sidekiq_pidfile="$app_root/tmp/pids/sidekiq.pid" sidekiq_logfile="$app_root/log/sidekiq.log" +sidekiq_config="$app_root/config/sidekiq_queues.yml" gitlab_user=$(ls -l config.ru | awk '{print $3}') warn() @@ -37,7 +38,7 @@ start_no_deamonize() start_sidekiq() { - exec bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile "$@" + exec bundle exec sidekiq -C "${sidekiq_config}" -e $RAILS_ENV -P $sidekiq_pidfile "$@" } load_ok() diff --git a/config/application.rb b/config/application.rb index 8a9c539cb43..92c8467e7f4 100644 --- a/config/application.rb +++ b/config/application.rb @@ -24,7 +24,8 @@ module Gitlab #{config.root}/app/models/ci #{config.root}/app/models/hooks #{config.root}/app/models/members - #{config.root}/app/models/project_services)) + #{config.root}/app/models/project_services + #{config.root}/app/workers/concerns)) config.generators.templates.push("#{config.root}/generator_templates") @@ -87,6 +88,7 @@ module Gitlab config.assets.precompile << "users/users_bundle.js" config.assets.precompile << "network/network_bundle.js" config.assets.precompile << "profile/profile_bundle.js" + config.assets.precompile << "protected_branches/protected_branches_bundle.js" config.assets.precompile << "diff_notes/diff_notes_bundle.js" config.assets.precompile << "boards/boards_bundle.js" config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js" diff --git a/config/mail_room.yml b/config/mail_room.yml index c639f8260aa..68697bd1dc4 100644 --- a/config/mail_room.yml +++ b/config/mail_room.yml @@ -25,7 +25,7 @@ :delivery_options: :redis_url: <%= config[:redis_url].to_json %> :namespace: <%= Gitlab::Redis::SIDEKIQ_NAMESPACE %> - :queue: incoming_email + :queue: email_receiver :worker: EmailReceiverWorker :arbitration_method: redis diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml new file mode 100644 index 00000000000..f36fe893fd0 --- /dev/null +++ b/config/sidekiq_queues.yml @@ -0,0 +1,47 @@ +# This configuration file should be exclusively used to set queue settings for +# Sidekiq. Any other setting should be specified using the Sidekiq CLI or the +# Sidekiq Ruby API (see config/initializers/sidekiq.rb). +--- +# All the queues to process and their weights. Every queue _must_ have a weight +# defined. +# +# The available weights are as follows +# +# 1: low priority +# 2: medium priority +# 3: high priority +# 5: _super_ high priority, this should only be used for _very_ important queues +# +# As per http://stackoverflow.com/a/21241357/290102 the formula for calculating +# the likelihood of a job being popped off a queue (given all queues have work +# to perform) is: +# +# chance = (queue weight / total weight of all queues) * 100 +:queues: + - [post_receive, 5] + - [merge, 5] + - [update_merge_requests, 3] + - [new_note, 2] + - [build, 2] + - [pipeline, 2] + - [gitlab_shell, 2] + - [email_receiver, 2] + - [emails_on_push, 2] + - [mailers, 2] + - [repository_fork, 1] + - [repository_import, 1] + - [project_service, 1] + - [clear_database_cache, 1] + - [delete_user, 1] + - [expire_build_instance_artifacts, 1] + - [group_destroy, 1] + - [irker, 1] + - [project_cache, 1] + - [project_destroy, 1] + - [project_export, 1] + - [project_web_hook, 1] + - [repository_check, 1] + - [system_hook, 1] + - [git_garbage_collect, 1] + - [cronjob, 1] + - [default, 1] diff --git a/db/migrate/20161017125927_add_unique_index_to_labels.rb b/db/migrate/20161017125927_add_unique_index_to_labels.rb index 16ae38612de..f2b56ebfb7b 100644 --- a/db/migrate/20161017125927_add_unique_index_to_labels.rb +++ b/db/migrate/20161017125927_add_unique_index_to_labels.rb @@ -7,9 +7,9 @@ class AddUniqueIndexToLabels < ActiveRecord::Migration disable_ddl_transaction! def up - select_all('SELECT title, COUNT(id) as cnt FROM labels GROUP BY project_id, title HAVING COUNT(id) > 1').each do |label| + select_all('SELECT title, project_id, COUNT(id) as cnt FROM labels GROUP BY project_id, title HAVING COUNT(id) > 1').each do |label| label_title = quote_string(label['title']) - duplicated_ids = select_all("SELECT id FROM labels WHERE title = '#{label_title}' ORDER BY id ASC").map{ |label| label['id'] } + duplicated_ids = select_all("SELECT id FROM labels WHERE project_id = #{label['project_id']} AND title = '#{label_title}' ORDER BY id ASC").map{ |label| label['id'] } label_id = duplicated_ids.first duplicated_ids.delete(label_id) diff --git a/db/migrate/20161018124658_make_project_owners_masters.rb b/db/migrate/20161018124658_make_project_owners_masters.rb new file mode 100644 index 00000000000..a576bb7b622 --- /dev/null +++ b/db/migrate/20161018124658_make_project_owners_masters.rb @@ -0,0 +1,15 @@ +class MakeProjectOwnersMasters < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + update_column_in_batches(:members, :access_level, 40) do |table, query| + query.where(table[:access_level].eq(50).and(table[:source_type].eq('Project'))) + end + end + + def down + # do nothing + end +end diff --git a/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb b/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb new file mode 100644 index 00000000000..e875213ab96 --- /dev/null +++ b/db/migrate/20161019190736_migrate_sidekiq_queues_from_default.rb @@ -0,0 +1,109 @@ +require 'json' + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MigrateSidekiqQueuesFromDefault < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + + DOWNTIME_REASON = <<-EOF + Moving Sidekiq jobs from queues requires Sidekiq to be stopped. Not stopping + Sidekiq will result in the loss of jobs that are scheduled after this + migration completes. + EOF + + disable_ddl_transaction! + + # Jobs for which the queue names have been changed (e.g. multiple workers + # using the same non-default queue). + # + # The keys are the old queue names, the values the jobs to move and their new + # queue names. + RENAMED_QUEUES = { + gitlab_shell: { + 'GitGarbageCollectorWorker' => :git_garbage_collector, + 'ProjectExportWorker' => :project_export, + 'RepositoryForkWorker' => :repository_fork, + 'RepositoryImportWorker' => :repository_import + }, + project_web_hook: { + 'ProjectServiceWorker' => :project_service + }, + incoming_email: { + 'EmailReceiverWorker' => :email_receiver + }, + mailers: { + 'EmailsOnPushWorker' => :emails_on_push + }, + default: { + 'AdminEmailWorker' => :cronjob, + 'BuildCoverageWorker' => :build, + 'BuildEmailWorker' => :build, + 'BuildFinishedWorker' => :build, + 'BuildHooksWorker' => :build, + 'BuildSuccessWorker' => :build, + 'ClearDatabaseCacheWorker' => :clear_database_cache, + 'DeleteUserWorker' => :delete_user, + 'ExpireBuildArtifactsWorker' => :cronjob, + 'ExpireBuildInstanceArtifactsWorker' => :expire_build_instance_artifacts, + 'GroupDestroyWorker' => :group_destroy, + 'ImportExportProjectCleanupWorker' => :cronjob, + 'IrkerWorker' => :irker, + 'MergeWorker' => :merge, + 'NewNoteWorker' => :new_note, + 'PipelineHooksWorker' => :pipeline, + 'PipelineMetricsWorker' => :pipeline, + 'PipelineProcessWorker' => :pipeline, + 'PipelineSuccessWorker' => :pipeline, + 'PipelineUpdateWorker' => :pipeline, + 'ProjectCacheWorker' => :project_cache, + 'ProjectDestroyWorker' => :project_destroy, + 'PruneOldEventsWorker' => :cronjob, + 'RemoveExpiredGroupLinksWorker' => :cronjob, + 'RemoveExpiredMembersWorker' => :cronjob, + 'RepositoryArchiveCacheWorker' => :cronjob, + 'RepositoryCheck::BatchWorker' => :cronjob, + 'RepositoryCheck::ClearWorker' => :repository_check, + 'RepositoryCheck::SingleRepositoryWorker' => :repository_check, + 'RequestsProfilesWorker' => :cronjob, + 'StuckCiBuildsWorker' => :cronjob, + 'UpdateMergeRequestsWorker' => :update_merge_requests + } + } + + def up + Sidekiq.redis do |redis| + RENAMED_QUEUES.each do |queue, jobs| + migrate_from_queue(redis, queue, jobs) + end + end + end + + def down + Sidekiq.redis do |redis| + RENAMED_QUEUES.each do |dest_queue, jobs| + jobs.each do |worker, from_queue| + migrate_from_queue(redis, from_queue, worker => dest_queue) + end + end + end + end + + def migrate_from_queue(redis, queue, job_mapping) + while job = redis.lpop("queue:#{queue}") + payload = JSON.load(job) + new_queue = job_mapping[payload['class']] + + # If we have no target queue to migrate to we're probably dealing with + # some ancient job for which the worker no longer exists. In that case + # there's no sane option we can take, other than just dropping the job. + next unless new_queue + + payload['queue'] = new_queue + + redis.lpush("queue:#{new_queue}", JSON.dump(payload)) + end + end +end diff --git a/db/migrate/20161019213545_generate_project_feature_for_projects.rb b/db/migrate/20161019213545_generate_project_feature_for_projects.rb new file mode 100644 index 00000000000..4554e14b0df --- /dev/null +++ b/db/migrate/20161019213545_generate_project_feature_for_projects.rb @@ -0,0 +1,28 @@ +class GenerateProjectFeatureForProjects < ActiveRecord::Migration + DOWNTIME = true + + DOWNTIME_REASON = <<-HEREDOC + Application was eager loading project_feature for all projects generating an extra query + everytime a project was fetched. We removed that behavior to avoid the extra query, this migration + makes sure all projects have a project_feature record associated. + HEREDOC + + def up + # Generate enabled values for each project feature 20, 20, 20, 20, 20 + # All features are enabled by default + enabled_values = [ProjectFeature::ENABLED] * 5 + + execute <<-EOF.strip_heredoc + INSERT INTO project_features + (project_id, merge_requests_access_level, builds_access_level, + issues_access_level, snippets_access_level, wiki_access_level) + (SELECT projects.id, #{enabled_values.join(',')} FROM projects LEFT OUTER JOIN project_features + ON project_features.project_id = projects.id + WHERE project_features.id IS NULL) + EOF + end + + def down + "Not needed" + end +end diff --git a/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb b/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb new file mode 100644 index 00000000000..06d07bdb835 --- /dev/null +++ b/db/migrate/20161024042317_migrate_mailroom_queue_from_default.rb @@ -0,0 +1,63 @@ +require 'json' + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MigrateMailroomQueueFromDefault < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = true + + DOWNTIME_REASON = <<-EOF + Moving Sidekiq jobs from queues requires Sidekiq to be stopped. Not stopping + Sidekiq will result in the loss of jobs that are scheduled after this + migration completes. + EOF + + disable_ddl_transaction! + + # Jobs for which the queue names have been changed (e.g. multiple workers + # using the same non-default queue). + # + # The keys are the old queue names, the values the jobs to move and their new + # queue names. + RENAMED_QUEUES = { + incoming_email: { + 'EmailReceiverWorker' => :email_receiver + } + } + + def up + Sidekiq.redis do |redis| + RENAMED_QUEUES.each do |queue, jobs| + migrate_from_queue(redis, queue, jobs) + end + end + end + + def down + Sidekiq.redis do |redis| + RENAMED_QUEUES.each do |dest_queue, jobs| + jobs.each do |worker, from_queue| + migrate_from_queue(redis, from_queue, worker => dest_queue) + end + end + end + end + + def migrate_from_queue(redis, queue, job_mapping) + while job = redis.lpop("queue:#{queue}") + payload = JSON.load(job) + new_queue = job_mapping[payload['class']] + + # If we have no target queue to migrate to we're probably dealing with + # some ancient job for which the worker no longer exists. In that case + # there's no sane option we can take, other than just dropping the job. + next unless new_queue + + payload['queue'] = new_queue + + redis.lpush("queue:#{new_queue}", JSON.dump(payload)) + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 65f55aa109b..02282b0f666 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: 20161018024550) do +ActiveRecord::Schema.define(version: 20161024042317) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -843,7 +843,7 @@ ActiveRecord::Schema.define(version: 20161018024550) do t.integer "builds_access_level" t.datetime "created_at" t.datetime "updated_at" - t.integer "repository_access_level", default: 20, null: false + t.integer "repository_access_level", default: 20, null: false end add_index "project_features", ["project_id"], name: "index_project_features_on_project_id", using: :btree diff --git a/doc/administration/integration/koding.md b/doc/administration/integration/koding.md index a2c358af095..b95c425842c 100644 --- a/doc/administration/integration/koding.md +++ b/doc/administration/integration/koding.md @@ -61,6 +61,7 @@ executing commands in the following snippet. ```bash git clone https://github.com/koding/koding.git cd koding +docker-compose -f docker-compose-init.yml run init docker-compose up ``` diff --git a/doc/api/builds.md b/doc/api/builds.md index e40f198696d..0476cac0eda 100644 --- a/doc/api/builds.md +++ b/doc/api/builds.md @@ -11,10 +11,10 @@ GET /projects/:id/builds | Attribute | Type | Required | Description | |-----------|---------|----------|---------------------| | `id` | integer | yes | The ID of a project | -| `scope` | string **or** array of strings | no | The scope of builds to show, one or array of: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided | +| `scope` | string **or** array of strings | no | The scope of builds to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all builds if none provided | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v3/projects/1/builds?scope%5B0%5D=pending&scope%5B1%5D=running' ``` Example of response @@ -132,10 +132,10 @@ GET /projects/:id/repository/commits/:sha/builds |-----------|---------|----------|---------------------| | `id` | integer | yes | The ID of a project | | `sha` | string | yes | The SHA id of a commit | -| `scope` | string **or** array of strings | no | The scope of builds to show, one or array of: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided | +| `scope` | string **or** array of strings | no | The scope of builds to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all builds if none provided | ``` -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds" +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v3/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds?scope%5B0%5D=pending&scope%5B1%5D=running' ``` Example of response diff --git a/doc/api/commits.md b/doc/api/commits.md index 6e0882a94de..e1ed99d98d3 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -319,7 +319,7 @@ GET /projects/:id/repository/commits/:sha/statuses | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user | `sha` | string | yes | The commit SHA -| `ref_name`| string | no | The name of a repository branch or tag or, if not given, the default branch +| `ref` | string | no | The name of a repository branch or tag or, if not given, the default branch | `stage` | string | no | Filter by [build stage](../ci/yaml/README.md#stages), e.g., `test` | `name` | string | no | Filter by [job name](../ci/yaml/README.md#jobs), e.g., `bundler:audit` | `all` | boolean | no | Return all statuses, not only the latest ones diff --git a/doc/api/users.md b/doc/api/users.md index 2b12770d5a5..a50ba5432fe 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -643,7 +643,7 @@ Parameters: | `id` | integer | yes | The ID of the user | ```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/user/:id/events +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/users/:id/events ``` Example response: diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index 0f64137a8a9..79bbe8421c6 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -188,7 +188,7 @@ In order to do that, follow the steps: image = "docker:latest" privileged = false disable_cache = false - volumes = ["/var/run/docker.sock", "/cache"] + volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"] [runners.cache] Insecure = false ``` diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 520c8b36a95..aba77490915 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -37,7 +37,7 @@ The registered runner will use the `ruby:2.1` docker image and will run two services, `postgres:latest` and `mysql:latest`, both of which will be accessible during the build process. -## What is image +## What is an image The `image` keyword is the name of the docker image that is present in the local Docker Engine (list all images with `docker images`) or any image that @@ -47,7 +47,7 @@ Hub please read the [Docker Fundamentals][] documentation. In short, with `image` we refer to the docker image, which will be used to create a container on which your build will run. -## What is service +## What is a service The `services` keyword defines just another docker image that is run during your build and is linked to the docker image that the `image` keyword defines. @@ -61,7 +61,7 @@ time the project is built. You can see some widely used services examples in the relevant documentation of [CI services examples](../services/README.md). -### How is service linked to the build +### How services are linked to the build To better understand how the container linking works, read [Linking containers together][linking-containers]. diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 84ea59ab687..5c0e1c44e3f 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -146,13 +146,17 @@ variables: ``` These variables can be later used in all executed commands and scripts. - The YAML-defined variables are also set to all created service containers, -thus allowing to fine tune them. +thus allowing to fine tune them. Variables can be also defined on a +[job level](#job-variables). -Variables can be also defined on [job level](#job-variables). +Except for the user defined variables, there are also the ones set up by the +Runner itself. One example would be `CI_BUILD_REF_NAME` which has the value of +the branch or tag name for which project is built. Apart from the variables +you can set in `.gitlab-ci.yml`, there are also the so called secret variables +which can be set in GitLab's UI. -[Learn more about variables.](../variables/README.md) +[Learn more about variables.][variables] ### cache @@ -541,20 +545,29 @@ An example usage of manual actions is deployment to production. > Introduced in GitLab 8.9. -`environment` is used to define that a job deploys to a specific [environment]. -This allows easy tracking of all deployments to your environments straight from -GitLab. +> You can read more about environments and find more examples in the +[documentation about environments][environment]. +`environment` is used to define that a job deploys to a specific environment. If `environment` is specified and no environment under that name exists, a new one will be created automatically. -The `environment` name must contain only letters, digits, '-', '_', '/', '$', '{', '}' and spaces. Common -names are `qa`, `staging`, and `production`, but you can use whatever name works -with your workflow. +The `environment` name can contain: ---- +- letters +- digits +- spaces +- `-` +- `_` +- `/` +- `$` +- `{` +- `}` -**Example configurations** +Common names are `qa`, `staging`, and `production`, but you can use whatever +name works with your workflow. + +In its simplest form, the `environment` keyword can be defined like: ``` deploy to production: @@ -563,39 +576,134 @@ deploy to production: environment: production ``` -The `deploy to production` job will be marked as doing deployment to -`production` environment. +In the above example, the `deploy to production` job will be marked as doing a +deployment to the `production` environment. + +#### environment:name + +> Introduced in GitLab 8.11. + +>**Note:** +Before GitLab 8.11, the name of an environment could be defined as a string like +`environment: production`. The recommended way now is to define it under the +`name` keyword. + +Instead of defining the name of the environment right after the `environment` +keyword, it is also possible to define it as a separate value. For that, use +the `name` keyword under `environment`: + +``` +deploy to production: + stage: deploy + script: git push production HEAD:master + environment: + name: production +``` + +#### environment:url + +> Introduced in GitLab 8.11. + +>**Note:** +Before GitLab 8.11, the URL could be added only in GitLab's UI. The +recommended way now is to define it in `.gitlab-ci.yml`. + +This is an optional value that when set, it exposes buttons in various places +in GitLab which when clicked take you to the defined URL. + +In the example below, if the job finishes successfully, it will create buttons +in the merge requests and in the environments/deployments pages which will point +to `https://prod.example.com`. + +``` +deploy to production: + stage: deploy + script: git push production HEAD:master + environment: + name: production + url: https://prod.example.com +``` + +#### environment:on_stop + +> [Introduced][ce-6669] in GitLab 8.13. + +Closing (stoping) environments can be achieved with the `on_stop` keyword defined under +`environment`. It declares a different job that runs in order to close +the environment. + +Read the `environment:action` section for an example. + +#### environment:action + +> [Introduced][ce-6669] in GitLab 8.13. + +The `action` keyword is to be used in conjunction with `on_stop` and is defined +in the job that is called to close the environment. + +Take for instance: + +```yaml +review_app: + stage: deploy + script: make deploy-app + environment: + name: review + on_stop: stop_review_app + +stop_review_app: + stage: deploy + script: make delete-app + when: manual + environment: + name: review + action: stop +``` + +In the above example we set up the `review_app` job to deploy to the `review` +environment, and we also defined a new `stop_review_app` job under `on_stop`. +Once the `review_app` job is successfully finished, it will trigger the +`stop_review_app` job based on what is defined under `when`. In this case we +set it up to `manual` so it will need a [manual action](#manual-actions) via +GitLab's web interface in order to run. + +The `stop_review_app` job is **required** to have the following keywords defined: + +- `when` - [reference](#when) +- `environment:name` +- `environment:action` #### dynamic environments > [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6. `environment` can also represent a configuration hash with `name` and `url`. -These parameters can use any of the defined CI [variables](#variables) +These parameters can use any of the defined [CI variables](#variables) (including predefined, secure variables and `.gitlab-ci.yml` variables). -The common use case is to create dynamic environments for branches and use them -as review apps. - ---- - -**Example configurations** +For example: ``` deploy as review app: stage: deploy - script: ... + script: make deploy environment: name: review-apps/$CI_BUILD_REF_NAME url: https://$CI_BUILD_REF_NAME.review.example.com/ ``` The `deploy as review app` job will be marked as deployment to dynamically -create the `review-apps/branch-name` environment. +create the `review-apps/$CI_BUILD_REF_NAME` environment, which `$CI_BUILD_REF_NAME` +is an [environment variable][variables] set by the Runner. If for example the +`deploy as review app` job was run in a branch named `pow`, this environment +should be accessible under `https://pow.review.example.com/`. -This environment should be accessible under `https://branch-name.review.example.com/`. +This of course implies that the underlying server which hosts the application +is properly configured. -You can see a simple example at https://gitlab.com/gitlab-examples/review-apps-nginx/. +The common use case is to create dynamic environments for branches and use them +as Review Apps. You can see a simple example using Review Apps at +https://gitlab.com/gitlab-examples/review-apps-nginx/. ### artifacts @@ -1105,3 +1213,5 @@ CI with various languages. [examples]: ../examples/README.md [ce-6323]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6323 [environment]: ../environments.md +[ce-6669]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6669 +[variables]: ../variables/README.md diff --git a/doc/development/README.md b/doc/development/README.md index 9706cb1de7f..14d6f08e43a 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -8,13 +8,16 @@ ## Styleguides +- [API styleguide](api_styleguide.md) Use this styleguide if you are + contributing to the API. - [Documentation styleguide](doc_styleguide.md) Use this styleguide if you are contributing to documentation. - [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations - [Testing standards and style guidelines](testing.md) - [UI guide](ui_guide.md) for building GitLab with existing CSS styles and elements - [Frontend guidelines](frontend.md) -- [SQL guidelines](sql.md) for SQL guidelines +- [SQL guidelines](sql.md) for working with SQL queries +- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers ## Process diff --git a/doc/development/api_styleguide.md b/doc/development/api_styleguide.md new file mode 100644 index 00000000000..ce444ebdde4 --- /dev/null +++ b/doc/development/api_styleguide.md @@ -0,0 +1,96 @@ +# API styleguide + +This styleguide recommends best practices for API development. + +## Instance variables + +Please do not use instance variables, there is no need for them (we don't need +to access them as we do in Rails views), local variables are fine. + +## Entities + +Always use an [Entity] to present the endpoint's payload. + +## Methods and parameters description + +Every method must be described using the [Grape DSL](https://github.com/ruby-grape/grape#describing-methods) +(see https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/api/environments.rb +for a good example): + +- `desc` for the method summary. You should pass it a block for additional + details such as: + - The GitLab version when the endpoint was added + - If the endpoint is deprecated, and if so, when will it be removed + +- `params` for the method params. This acts as description, + [validation, and coercion of the parameters] + +A good example is as follows: + +```ruby +desc 'Get all broadcast messages' do + detail 'This feature was introduced in GitLab 8.12.' + success Entities::BroadcastMessage +end +params do + optional :page, type: Integer, desc: 'Current page number' + optional :per_page, type: Integer, desc: 'Number of messages per page' +end +get do + messages = BroadcastMessage.all + + present paginate(messages), with: Entities::BroadcastMessage +end +``` + +## Declared params + +> Grape allows you to access only the parameters that have been declared by your +`params` block. It filters out the params that have been passed, but are not +allowed. + +– https://github.com/ruby-grape/grape#declared + +### Exclude params from parent namespaces! + +> By default `declared(params) `includes parameters that were defined in all +parent namespaces. + +– https://github.com/ruby-grape/grape#include-parent-namespaces + +In most cases you will want to exclude params from the parent namespaces: + +```ruby +declared(params, include_parent_namespaces: false) +``` + +### When to use `declared(params)`? + +You should always use `declared(params)` when you pass the params hash as +arguments to a method call. + +For instance: + +```ruby +# bad +User.create(params) # imagine the user submitted `admin=1`... :) + +# good +User.create(declared(params, include_parent_namespaces: false).to_h) +``` + +>**Note:** +`declared(params)` return a `Hashie::Mash` object, on which you will have to +call `.to_h`. + +But we can use `params[key]` directly when we access single elements. + +For instance: + +```ruby +# good +Model.create(foo: params[:foo]) +``` + +[Entity]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/api/entities.rb +[validation, and coercion of the parameters]: https://github.com/ruby-grape/grape#parameter-validation-and-coercion diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index f07d2c9af2d..4cc581dd991 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -342,12 +342,6 @@ You can use the following fake tokens as examples. Here is a list of must-have items. Use them in the exact order that appears on this document. Further explanation is given below. -- Every method must be described using [Grape's DSL](https://github.com/ruby-grape/grape/tree/v0.13.0#describing-methods) - (see https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/api/environments.rb - for a good example): - - `desc` for the method summary (you can pass it a block for additional details) - - `params` for the method params (this acts as description **and** validation - of the params) - Every method must have the REST API request. For example: ``` diff --git a/doc/development/performance.md b/doc/development/performance.md index c4a964d1da3..8337c2d9cb3 100644 --- a/doc/development/performance.md +++ b/doc/development/performance.md @@ -37,7 +37,7 @@ graphs/dashboards. GitLab provides built-in tools to aid the process of improving performance: * [Sherlock](profiling.md#sherlock) -* [GitLab Performance Monitoring](../monitoring/performance/monitoring.md) +* [GitLab Performance Monitoring](../administration/monitoring/performance/monitoring.md) * [Request Profiling](../administration/monitoring/performance/request_profiling.md) GitLab employees can use GitLab.com's performance monitoring systems located at diff --git a/doc/development/sidekiq_style_guide.md b/doc/development/sidekiq_style_guide.md new file mode 100644 index 00000000000..e3a20f29a09 --- /dev/null +++ b/doc/development/sidekiq_style_guide.md @@ -0,0 +1,38 @@ +# Sidekiq Style Guide + +This document outlines various guidelines that should be followed when adding or +modifying Sidekiq workers. + +## Default Queue + +Use of the "default" queue is not allowed. Every worker should use a queue that +matches the worker's purpose the closest. For example, workers that are to be +executed periodically should use the "cronjob" queue. + +A list of all available queues can be found in `config/sidekiq_queues.yml`. + +## Dedicated Queues + +Most workers should use their own queue. To ease this process a worker can +include the `DedicatedSidekiqQueue` concern as follows: + +```ruby +class ProcessSomethingWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue +end +``` + +This will set the queue name based on the class' name, minus the `Worker` +suffix. In the above example this would lead to the queue being +`process_something`. + +In some cases multiple workers do use the same queue. For example, the various +workers for updating CI pipelines all use the `pipeline` queue. Adding workers +to existing queues should be done with care, as adding more workers can lead to +slow jobs blocking work (even for different jobs) on the shared queue. + +## Tests + +Each Sidekiq worker must be tested using RSpec, just like any other class. These +tests should be placed in `spec/workers`. diff --git a/doc/monitoring/performance/gitlab_configuration.md b/doc/monitoring/performance/gitlab_configuration.md index a669bb28904..19d46135930 100644 --- a/doc/monitoring/performance/gitlab_configuration.md +++ b/doc/monitoring/performance/gitlab_configuration.md @@ -1 +1 @@ -This document was moved to [administration/monitoring/performance/gitlab_configuration](../administration/monitoring/performance/gitlab_configuration.md). +This document was moved to [administration/monitoring/performance/gitlab_configuration](../../administration/monitoring/performance/gitlab_configuration.md). diff --git a/doc/monitoring/performance/influxdb_configuration.md b/doc/monitoring/performance/influxdb_configuration.md index 02647de1eb0..15fd275e916 100644 --- a/doc/monitoring/performance/influxdb_configuration.md +++ b/doc/monitoring/performance/influxdb_configuration.md @@ -1 +1 @@ -This document was moved to [administration/monitoring/performance/influxdb_configuration](../administration/monitoring/performance/influxdb_configuration.md). +This document was moved to [administration/monitoring/performance/influxdb_configuration](../../administration/monitoring/performance/influxdb_configuration.md). diff --git a/doc/monitoring/performance/influxdb_schema.md b/doc/monitoring/performance/influxdb_schema.md index a989e323e04..e53f9701dc3 100644 --- a/doc/monitoring/performance/influxdb_schema.md +++ b/doc/monitoring/performance/influxdb_schema.md @@ -1 +1 @@ -This document was moved to [administration/monitoring/performance/influxdb_schema](../administration/monitoring/performance/influxdb_schema.md). +This document was moved to [administration/monitoring/performance/influxdb_schema](../../administration/monitoring/performance/influxdb_schema.md). diff --git a/doc/monitoring/performance/introduction.md b/doc/monitoring/performance/introduction.md index ab3f3ac1664..ae88baa0c14 100644 --- a/doc/monitoring/performance/introduction.md +++ b/doc/monitoring/performance/introduction.md @@ -1 +1 @@ -This document was moved to [administration/monitoring/performance/introduction](../administration/monitoring/performance/introduction.md). +This document was moved to [administration/monitoring/performance/introduction](../../administration/monitoring/performance/introduction.md). diff --git a/doc/project_services/img/builds_emails_service.png b/doc/project_services/img/builds_emails_service.png index 88943dc410e..440728795be 100644 Binary files a/doc/project_services/img/builds_emails_service.png and b/doc/project_services/img/builds_emails_service.png differ diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 26baffdf792..0ad84705cfd 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -30,6 +30,10 @@ Use this if you've installed GitLab from source: ``` sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production ``` +If you are running GitLab within a Docker container, you can run the backup from the host: +``` +docker -t exec
please fix") + "
please fix") end end context 'merge request events' do - let(:merge_request) { create(:merge_request, description: 'please fix', title: 'Awesome merge request', target_project: project, source_project: project) } + let(:merge_request) { create(:merge_request, description: '**please** fix', title: 'Awesome merge request', target_project: project, source_project: project) } let(:merge_service) { MergeRequests::CreateService.new(project, user) } let(:merge_sample_data) { merge_service.hook_data(merge_request, 'open') } @@ -159,7 +159,7 @@ describe HipchatService, models: true do "merge request !#{obj_attr["iid"]} in " \ "#{project_name}: " \ "Awesome merge request" \ - "
please fix") + "
please fix") end end @@ -203,7 +203,7 @@ describe HipchatService, models: true do let(:merge_request_note) do create(:note_on_merge_request, noteable: merge_request, project: project, - note: "merge request note") + note: "merge request **note**") end it "calls Hipchat API for merge request comment events" do @@ -222,7 +222,7 @@ describe HipchatService, models: true do "merge request !#{merge_id} in " \ "#{project_name}: " \ "#{title}" \ - "
merge request note") + "
merge request note") end end @@ -230,7 +230,7 @@ describe HipchatService, models: true do let(:issue) { create(:issue, project: project) } let(:issue_note) do create(:note_on_issue, noteable: issue, project: project, - note: "issue note") + note: "issue **note**") end it "calls Hipchat API for issue comment events" do @@ -247,7 +247,7 @@ describe HipchatService, models: true do "issue ##{issue_id} in " \ "#{project_name}: " \ "#{title}" \ - "
issue note") + "
issue note") end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index e6d98e25d0b..f4dda1ee558 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -67,6 +67,14 @@ describe Project, models: true do it { is_expected.to have_many(:notification_settings).dependent(:destroy) } it { is_expected.to have_many(:forks).through(:forked_project_links) } + context 'after create' do + it "creates project feature" do + project = FactoryGirl.build(:project) + + expect { project.save }.to change{ project.project_feature.present? }.from(false).to(true) + end + end + describe '#members & #requesters' do let(:project) { create(:project, :public) } let(:requester) { create(:user) } @@ -531,9 +539,9 @@ describe Project, models: true do end describe '#has_wiki?' do - let(:no_wiki_project) { build(:project, wiki_enabled: false, has_external_wiki: false) } - let(:wiki_enabled_project) { build(:project) } - let(:external_wiki_project) { build(:project, has_external_wiki: true) } + let(:no_wiki_project) { create(:project, wiki_access_level: ProjectFeature::DISABLED, has_external_wiki: false) } + let(:wiki_enabled_project) { create(:project) } + let(:external_wiki_project) { create(:project, has_external_wiki: true) } it 'returns true if project is wiki enabled or has external wiki' do expect(wiki_enabled_project).to have_wiki diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index f977cf73673..187a1bf2d79 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1146,28 +1146,17 @@ describe Repository, models: true do end describe '#before_import' do - it 'flushes the emptiness cachess' do - expect(repository).to receive(:expire_emptiness_caches) - - repository.before_import - end - - it 'flushes the exists cache' do - expect(repository).to receive(:expire_exists_cache) + it 'flushes the repository caches' do + expect(repository).to receive(:expire_content_cache) repository.before_import end end describe '#after_import' do - it 'flushes the emptiness cachess' do - expect(repository).to receive(:expire_emptiness_caches) - - repository.after_import - end - - it 'flushes the exists cache' do - expect(repository).to receive(:expire_exists_cache) + it 'flushes and builds the cache' do + expect(repository).to receive(:expire_content_cache) + expect(repository).to receive(:build_cache) repository.after_import end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 65b2896930a..10c39b90212 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -15,11 +15,11 @@ describe User, models: true do describe 'associations' do it { is_expected.to have_one(:namespace) } - it { is_expected.to have_many(:snippets).class_name('Snippet').dependent(:destroy) } + it { is_expected.to have_many(:snippets).dependent(:destroy) } it { is_expected.to have_many(:project_members).dependent(:destroy) } it { is_expected.to have_many(:groups) } it { is_expected.to have_many(:keys).dependent(:destroy) } - it { is_expected.to have_many(:events).class_name('Event').dependent(:destroy) } + it { is_expected.to have_many(:events).dependent(:destroy) } it { is_expected.to have_many(:recent_events).class_name('Event') } it { is_expected.to have_many(:issues).dependent(:destroy) } it { is_expected.to have_many(:notes).dependent(:destroy) } diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 3fd989dd7a6..905f762d578 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -48,92 +48,154 @@ describe API::API, api: true do end describe 'PUT /projects/:id/repository/branches/:branch/protect' do - it 'protects a single branch' do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) - - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(false) - expect(json_response['developers_can_merge']).to eq(false) - end - - it 'protects a single branch and developers can push' do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), - developers_can_push: true - - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(true) - expect(json_response['developers_can_merge']).to eq(false) - end - - it 'protects a single branch and developers can merge' do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), - developers_can_merge: true - - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(false) - expect(json_response['developers_can_merge']).to eq(true) - end - - it 'protects a single branch and developers can push and merge' do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), - developers_can_push: true, developers_can_merge: true - - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(true) - expect(json_response['developers_can_merge']).to eq(true) - end - - it 'protects a single branch and developers cannot push and merge' do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), - developers_can_push: 'tru', developers_can_merge: 'tr' - - expect(response).to have_http_status(200) - expect(json_response['name']).to eq(branch_name) - expect(json_response['commit']['id']).to eq(branch_sha) - expect(json_response['protected']).to eq(true) - expect(json_response['developers_can_push']).to eq(false) - expect(json_response['developers_can_merge']).to eq(false) - end - - context 'on a protected branch' do - let(:protected_branch) { 'foo' } - - before do - project.repository.add_branch(user, protected_branch, 'master') - create(:protected_branch, :developers_can_push, :developers_can_merge, project: project, name: protected_branch) - end - - it 'updates that a developer can push' do - put api("/projects/#{project.id}/repository/branches/#{protected_branch}/protect", user), - developers_can_push: false, developers_can_merge: false + context "when a protected branch doesn't already exist" do + it 'protects a single branch' do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) expect(response).to have_http_status(200) - expect(json_response['name']).to eq(protected_branch) + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) expect(json_response['protected']).to eq(true) expect(json_response['developers_can_push']).to eq(false) expect(json_response['developers_can_merge']).to eq(false) end - it 'does not update that a developer can push' do - put api("/projects/#{project.id}/repository/branches/#{protected_branch}/protect", user), - developers_can_push: 'foobar', developers_can_merge: 'foo' + it 'protects a single branch and developers can push' do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), + developers_can_push: true expect(response).to have_http_status(200) - expect(json_response['name']).to eq(protected_branch) + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) expect(json_response['protected']).to eq(true) expect(json_response['developers_can_push']).to eq(true) + expect(json_response['developers_can_merge']).to eq(false) + end + + it 'protects a single branch and developers can merge' do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), + developers_can_merge: true + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(false) + expect(json_response['developers_can_merge']).to eq(true) + end + + it 'protects a single branch and developers can push and merge' do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), + developers_can_push: true, developers_can_merge: true + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(true) + expect(json_response['developers_can_merge']).to eq(true) + end + + it 'protects a single branch and developers cannot push and merge' do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), + developers_can_push: 'tru', developers_can_merge: 'tr' + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(false) + expect(json_response['developers_can_merge']).to eq(false) + end + end + + context 'for an existing protected branch' do + before do + project.repository.add_branch(user, protected_branch.name, 'master') + end + + context "when developers can push and merge" do + let(:protected_branch) { create(:protected_branch, :developers_can_push, :developers_can_merge, project: project, name: 'protected_branch') } + + it 'updates that a developer cannot push or merge' do + put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), + developers_can_push: false, developers_can_merge: false + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(protected_branch.name) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(false) + expect(json_response['developers_can_merge']).to eq(false) + end + + it "doesn't result in 0 access levels when 'developers_can_push' is switched off" do + put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), + developers_can_push: false + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(protected_branch.name) + expect(protected_branch.reload.push_access_levels.first).to be_present + expect(protected_branch.reload.push_access_levels.first.access_level).to eq(Gitlab::Access::MASTER) + end + + it "doesn't result in 0 access levels when 'developers_can_merge' is switched off" do + put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), + developers_can_merge: false + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(protected_branch.name) + expect(protected_branch.reload.merge_access_levels.first).to be_present + expect(protected_branch.reload.merge_access_levels.first.access_level).to eq(Gitlab::Access::MASTER) + end + end + + context "when developers cannot push or merge" do + let(:protected_branch) { create(:protected_branch, project: project, name: 'protected_branch') } + + it 'updates that a developer can push and merge' do + put api("/projects/#{project.id}/repository/branches/#{protected_branch.name}/protect", user), + developers_can_push: true, developers_can_merge: true + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(protected_branch.name) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(true) + expect(json_response['developers_can_merge']).to eq(true) + end + end + end + + context "multiple API calls" do + it "returns success when `protect` is called twice" do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(branch_name) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(false) + expect(json_response['developers_can_merge']).to eq(false) + end + + it "returns success when `protect` is called twice with `developers_can_push` turned on" do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_push: true + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_push: true + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(branch_name) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(true) + expect(json_response['developers_can_merge']).to eq(false) + end + + it "returns success when `protect` is called twice with `developers_can_merge` turned on" do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_merge: true + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), developers_can_merge: true + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(branch_name) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(false) expect(json_response['developers_can_merge']).to eq(true) end end @@ -147,12 +209,6 @@ describe API::API, api: true do put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user2) expect(response).to have_http_status(403) end - - it "returns success when protect branch again" do - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) - put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) - expect(response).to have_http_status(200) - end end describe "PUT /projects/:id/repository/branches/:branch/unprotect" do diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 66fa0c0c01f..a6e8550fac3 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -72,6 +72,17 @@ describe API::API, api: true do expect(json_response['message']).to include "\"since\" must be a timestamp in ISO 8601 format" end end + + context "path optional parameter" do + it "returns project commits matching provided path parameter" do + path = 'files/ruby/popen.rb' + + get api("/projects/#{project.id}/repository/commits?path=#{path}", user) + + expect(json_response.size).to eq(3) + expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") + end + end end describe "Create a commit with multiple files and actions" do diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index 1da9988978b..46641fcd846 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -22,8 +22,7 @@ describe API::API, api: true do expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.size).to eq(2) - expect(json_response.first['name']).to eq(group_label.name) - expect(json_response.second['name']).to eq(label1.name) + expect(json_response.map { |l| l['name'] }).to match_array([group_label.name, label1.name]) end end @@ -160,14 +159,14 @@ describe API::API, api: true do it 'returns 400 if no label name given' do put api("/projects/#{project.id}/labels", user), new_name: 'label2' expect(response).to have_http_status(400) - expect(json_response['message']).to eq('400 (Bad request) "name" not given') + expect(json_response['error']).to eq('name is missing') end it 'returns 400 if no new parameters given' do put api("/projects/#{project.id}/labels", user), name: 'label1' expect(response).to have_http_status(400) - expect(json_response['message']).to eq('Required parameters '\ - '"new_name" or "color" missing') + expect(json_response['error']).to eq('new_name, color, description are missing, '\ + 'at least one parameter must be provided') end it 'returns 400 for invalid name' do diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index d22e0595788..493c0a893d1 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -328,4 +328,15 @@ describe API::Members, api: true do it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'group' do let(:source) { group } end + + context 'Adding owner to project' do + it 'returns 403' do + expect do + post api("/projects/#{project.id}/members", master), + user_id: stranger.id, access_level: Member::OWNER + + expect(response).to have_http_status(422) + end.to change { project.members.count }.by(0) + end + end end diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index d58bedc3bf7..0124b7271b3 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -221,12 +221,23 @@ describe API::API, api: true do end end - context 'when the user is posting an award emoji' do + context 'when the user is posting an award emoji on an issue created by someone else' do + let(:issue2) { create(:issue, project: project) } + it 'returns an award emoji' do + post api("/projects/#{project.id}/issues/#{issue2.id}/notes", user), body: ':+1:' + + expect(response).to have_http_status(201) + expect(json_response['awardable_id']).to eq issue2.id + end + end + + context 'when the user is posting an award emoji on his/her own issue' do + it 'creates a new issue note' do post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: ':+1:' expect(response).to have_http_status(201) - expect(json_response['awardable_id']).to eq issue.id + expect(json_response['body']).to eq(':+1:') end end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index f83f4d2c9b1..ae8639d78d5 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -846,7 +846,7 @@ describe API::API, api: true do end end - describe 'PUT /user/:id/block' do + describe 'PUT /users/:id/block' do before { admin } it 'blocks existing user' do put api("/users/#{user.id}/block", admin) @@ -873,7 +873,7 @@ describe API::API, api: true do end end - describe 'PUT /user/:id/unblock' do + describe 'PUT /users/:id/unblock' do let(:blocked_user) { create(:user, state: 'blocked') } before { admin } @@ -914,7 +914,7 @@ describe API::API, api: true do end end - describe 'GET /user/:id/events' do + describe 'GET /users/:id/events' do let(:user) { create(:user) } let(:project) { create(:empty_project) } let(:note) { create(:note_on_issue, note: 'What an awesome day!', project: project) } @@ -958,6 +958,29 @@ describe API::API, api: true do expect(joined_event['author']['name']).to eq(user.name) end end + + context 'when there are multiple events from different projects' do + let(:second_note) { create(:note_on_issue, project: create(:empty_project)) } + let(:third_note) { create(:note_on_issue, project: project) } + + before do + second_note.project.add_user(user, :developer) + + [second_note, third_note].each do |note| + EventCreateService.new.leave_note(note, user) + end + end + + it 'returns events in the correct order (from newest to oldest)' do + get api("/users/#{user.id}/events", user) + + comment_events = json_response.select { |e| e['action_name'] == 'commented on' } + + expect(comment_events[0]['target_id']).to eq(third_note.id) + expect(comment_events[1]['target_id']).to eq(second_note.id) + expect(comment_events[2]['target_id']).to eq(note.id) + end + end end it 'returns a 404 error if not found' do diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 93bf0f64963..f0ded06b785 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -23,14 +23,15 @@ describe Issues::MoveService, services: true do old_project.team << [user, :reporter] new_project.team << [user, :reporter] - ['label1', 'label2'].each do |label| + labels = Array.new(2) { |x| "label%d" % (x + 1) } + + labels.each do |label| old_issue.labels << create(:label, project_id: old_project.id, title: label) - end - new_project.labels << create(:label, title: 'label1') - new_project.labels << create(:label, title: 'label2') + new_project.labels << create(:label, title: label) + end end end @@ -207,10 +208,10 @@ describe Issues::MoveService, services: true do end end - describe 'rewritting references' do + describe 'rewriting references' do include_context 'issue move executed' - context 'issue reference' do + context 'issue references' do let(:another_issue) { create(:issue, project: old_project) } let(:description) { "Some description #{another_issue.to_reference}" } @@ -219,6 +220,16 @@ describe Issues::MoveService, services: true do .to eq "Some description #{old_project.to_reference}#{another_issue.to_reference}" end end + + context "user references" do + let(:another_issue) { create(:issue, project: old_project) } + let(:description) { "Some description #{user.to_reference}" } + + it "doesn't throw any errors for issues containing user references" do + expect(new_issue.description) + .to eq "Some description #{user.to_reference}" + end + end end context 'moving to same project' do @@ -277,5 +288,25 @@ describe Issues::MoveService, services: true do it { expect { move }.to raise_error(StandardError, /permissions/) } end end + + context 'movable issue with no assigned labels' do + before do + old_project.team << [user, :reporter] + new_project.team << [user, :reporter] + + labels = Array.new(2) { |x| "label%d" % (x + 1) } + + labels.each do |label| + new_project.labels << create(:label, title: label) + end + end + + include_context 'issue move executed' + + it 'does not assign labels to new issue' do + expected_label_titles = new_issue.reload.labels.map(&:title) + expect(expected_label_titles.size).to eq 0 + end + end end end diff --git a/spec/services/merge_requests/assign_issues_service_spec.rb b/spec/services/merge_requests/assign_issues_service_spec.rb index 7aeb95a15ea..5034b6ef33f 100644 --- a/spec/services/merge_requests/assign_issues_service_spec.rb +++ b/spec/services/merge_requests/assign_issues_service_spec.rb @@ -46,4 +46,16 @@ describe MergeRequests::AssignIssuesService, services: true do it 'assigns these to the merge request owner' do expect { service.execute }.to change { issue.reload.assignee }.to(user) end + + it 'ignores external issues' do + external_issue = ExternalIssue.new('JIRA-123', project) + service = described_class.new( + project, + user, + merge_request: merge_request, + closes_issues: [external_issue] + ) + + expect(service.assignable_issues.count).to eq 0 + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b19f5824236..06d52f0f735 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -50,6 +50,12 @@ RSpec.configure do |config| example.run Rails.cache = caching_store end + + config.around(:each, :redis) do |example| + Gitlab::Redis.with(&:flushall) + example.run + Gitlab::Redis.with(&:flushall) + end end FactoryGirl::SyntaxRunner.class_eval do diff --git a/spec/mailers/shared/notify.rb b/spec/support/notify_shared_examples.rb similarity index 100% rename from spec/mailers/shared/notify.rb rename to spec/support/notify_shared_examples.rb diff --git a/spec/support/select2_helper.rb b/spec/support/select2_helper.rb index 35cc51725c6..d30cc8ff9f2 100644 --- a/spec/support/select2_helper.rb +++ b/spec/support/select2_helper.rb @@ -17,9 +17,9 @@ module Select2Helper selector = options.fetch(:from) if options[:multiple] - execute_script("$('#{selector}').select2('val', ['#{value}'], true);") + execute_script("$('#{selector}').select2('val', ['#{value}']).trigger('change');") else - execute_script("$('#{selector}').select2('val', '#{value}', true);") + execute_script("$('#{selector}').select2('val', '#{value}').trigger('change');") end end end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 548e7780c36..73bc8326f02 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -9,6 +9,7 @@ describe 'gitlab:app namespace rake task' do Rake.application.rake_require 'tasks/gitlab/backup' Rake.application.rake_require 'tasks/gitlab/shell' Rake.application.rake_require 'tasks/gitlab/db' + Rake.application.rake_require 'tasks/cache' # empty task as env is already loaded Rake::Task.define_task :environment diff --git a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb new file mode 100644 index 00000000000..6f70b3daf8e --- /dev/null +++ b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe 'projects/merge_requests/show/_commits.html.haml' do + include Devise::Test::ControllerHelpers + + let(:user) { create(:user) } + let(:target_project) { create(:project) } + + let(:source_project) do + create(:project, forked_from_project: target_project) + end + + let(:merge_request) do + create(:merge_request, :simple, + source_project: source_project, + target_project: target_project, + author: user) + end + + before do + controller.prepend_view_path('app/views/projects') + + assign(:merge_request, merge_request) + assign(:commits, merge_request.commits) + end + + it 'shows commits from source project' do + render + + commit = source_project.commit(merge_request.source_branch) + href = namespace_project_commit_path( + source_project.namespace, + source_project, + commit) + + expect(rendered).to have_link(Commit.truncate_sha(commit.sha), href: href) + end +end diff --git a/spec/workers/concerns/build_queue_spec.rb b/spec/workers/concerns/build_queue_spec.rb new file mode 100644 index 00000000000..6bf955e0be2 --- /dev/null +++ b/spec/workers/concerns/build_queue_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe BuildQueue do + let(:worker) do + Class.new do + include Sidekiq::Worker + include BuildQueue + end + end + + it 'sets the queue name of a worker' do + expect(worker.sidekiq_options['queue'].to_s).to eq('build') + end +end diff --git a/spec/workers/concerns/cronjob_queue_spec.rb b/spec/workers/concerns/cronjob_queue_spec.rb new file mode 100644 index 00000000000..5d1336c21a6 --- /dev/null +++ b/spec/workers/concerns/cronjob_queue_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe CronjobQueue do + let(:worker) do + Class.new do + include Sidekiq::Worker + include CronjobQueue + end + end + + it 'sets the queue name of a worker' do + expect(worker.sidekiq_options['queue'].to_s).to eq('cronjob') + end + + it 'disables retrying of failed jobs' do + expect(worker.sidekiq_options['retry']).to eq(false) + end +end diff --git a/spec/workers/concerns/dedicated_sidekiq_queue_spec.rb b/spec/workers/concerns/dedicated_sidekiq_queue_spec.rb new file mode 100644 index 00000000000..512baec8b7e --- /dev/null +++ b/spec/workers/concerns/dedicated_sidekiq_queue_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe DedicatedSidekiqQueue do + let(:worker) do + Class.new do + def self.name + 'Foo::Bar::DummyWorker' + end + + include Sidekiq::Worker + include DedicatedSidekiqQueue + end + end + + describe 'queue names' do + it 'sets the queue name based on the class name' do + expect(worker.sidekiq_options['queue']).to eq('foo_bar_dummy') + end + end +end diff --git a/spec/workers/concerns/pipeline_queue_spec.rb b/spec/workers/concerns/pipeline_queue_spec.rb new file mode 100644 index 00000000000..40794d0e42a --- /dev/null +++ b/spec/workers/concerns/pipeline_queue_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe PipelineQueue do + let(:worker) do + Class.new do + include Sidekiq::Worker + include PipelineQueue + end + end + + it 'sets the queue name of a worker' do + expect(worker.sidekiq_options['queue'].to_s).to eq('pipeline') + end +end diff --git a/spec/workers/concerns/repository_check_queue_spec.rb b/spec/workers/concerns/repository_check_queue_spec.rb new file mode 100644 index 00000000000..8868e969829 --- /dev/null +++ b/spec/workers/concerns/repository_check_queue_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe RepositoryCheckQueue do + let(:worker) do + Class.new do + include Sidekiq::Worker + include RepositoryCheckQueue + end + end + + it 'sets the queue name of a worker' do + expect(worker.sidekiq_options['queue'].to_s).to eq('repository_check') + end + + it 'disables retrying of failed jobs' do + expect(worker.sidekiq_options['retry']).to eq(false) + end +end diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb new file mode 100644 index 00000000000..fc9adf47c1e --- /dev/null +++ b/spec/workers/every_sidekiq_worker_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe 'Every Sidekiq worker' do + let(:workers) do + root = Rails.root.join('app', 'workers') + concerns = root.join('concerns').to_s + + workers = Dir[root.join('**', '*.rb')]. + reject { |path| path.start_with?(concerns) } + + workers.map do |path| + ns = Pathname.new(path).relative_path_from(root).to_s.gsub('.rb', '') + + ns.camelize.constantize + end + end + + it 'does not use the default queue' do + workers.each do |worker| + expect(worker.sidekiq_options['queue'].to_s).not_to eq('default') + end + end + + it 'uses the cronjob queue when the worker runs as a cronjob' do + cron_workers = Settings.cron_jobs. + map { |job_name, options| options['job_class'].constantize }. + to_set + + workers.each do |worker| + next unless cron_workers.include?(worker) + + expect(worker.sidekiq_options['queue'].to_s).to eq('cronjob') + end + end + + it 'defines the queue in the Sidekiq configuration file' do + config = YAML.load_file(Rails.root.join('config', 'sidekiq_queues.yml').to_s) + queue_names = config[:queues].map { |(queue, _)| queue }.to_set + + workers.each do |worker| + expect(queue_names).to include(worker.sidekiq_options['queue'].to_s) + end + end +end diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb index 5785a6a06ff..bfa8c0ff2c6 100644 --- a/spec/workers/project_cache_worker_spec.rb +++ b/spec/workers/project_cache_worker_spec.rb @@ -5,22 +5,60 @@ describe ProjectCacheWorker do subject { described_class.new } - describe '#perform' do - it 'updates project cache data' do - expect_any_instance_of(Repository).to receive(:size) - expect_any_instance_of(Repository).to receive(:commit_count) + describe '.perform_async' do + it 'schedules the job when no lease exists' do + allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:exists?). + and_return(false) - expect_any_instance_of(Project).to receive(:update_repository_size) - expect_any_instance_of(Project).to receive(:update_commit_count) + expect_any_instance_of(described_class).to receive(:perform) - subject.perform(project.id) + described_class.perform_async(project.id) end - it 'handles missing repository data' do - expect_any_instance_of(Repository).to receive(:exists?).and_return(false) - expect_any_instance_of(Repository).not_to receive(:size) + it 'does not schedule the job when a lease exists' do + allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:exists?). + and_return(true) - subject.perform(project.id) + expect_any_instance_of(described_class).not_to receive(:perform) + + described_class.perform_async(project.id) + end + end + + describe '#perform' do + context 'when an exclusive lease can be obtained' do + before do + allow(subject).to receive(:try_obtain_lease_for).with(project.id). + and_return(true) + end + + it 'updates project cache data' do + expect_any_instance_of(Repository).to receive(:size) + expect_any_instance_of(Repository).to receive(:commit_count) + + expect_any_instance_of(Project).to receive(:update_repository_size) + expect_any_instance_of(Project).to receive(:update_commit_count) + + subject.perform(project.id) + end + + it 'handles missing repository data' do + expect_any_instance_of(Repository).to receive(:exists?).and_return(false) + expect_any_instance_of(Repository).not_to receive(:size) + + subject.perform(project.id) + end + end + + context 'when an exclusive lease can not be obtained' do + it 'does nothing' do + allow(subject).to receive(:try_obtain_lease_for).with(project.id). + and_return(false) + + expect(subject).not_to receive(:update_caches) + + subject.perform(project.id) + end end end end diff --git a/vendor/assets/javascripts/jquery.cookie.js b/vendor/assets/javascripts/jquery.cookie.js deleted file mode 100644 index 6a3e394b403..00000000000 --- a/vendor/assets/javascripts/jquery.cookie.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * jQuery Cookie plugin - * - * Copyright (c) 2010 Klaus Hartl (stilbuero.de) - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - * - */ -jQuery.cookie = function (key, value, options) { - - // key and at least value given, set cookie... - if (arguments.length > 1 && String(value) !== "[object Object]") { - options = jQuery.extend({}, options); - - if (value === null || value === undefined) { - options.expires = -1; - } - - if (typeof options.expires === 'number') { - var days = options.expires, t = options.expires = new Date(); - t.setDate(t.getDate() + days); - } - - value = String(value); - - return (document.cookie = [ - encodeURIComponent(key), '=', - options.raw ? value : encodeURIComponent(value), - options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE - options.path ? '; path=' + options.path : '', - options.domain ? '; domain=' + options.domain : '', - options.secure ? '; secure' : '' - ].join('')); - } - - // key and possibly options given, get cookie... - options = value || {}; - var result, decode = options.raw ? function (s) { return s; } : decodeURIComponent; - return (result = new RegExp('(?:^|; )' + encodeURIComponent(key) + '=([^;]*)').exec(document.cookie)) ? decode(result[1]) : null; -}; diff --git a/vendor/assets/javascripts/js.cookie.js b/vendor/assets/javascripts/js.cookie.js new file mode 100644 index 00000000000..92dbba162c4 --- /dev/null +++ b/vendor/assets/javascripts/js.cookie.js @@ -0,0 +1,156 @@ +/*! + * JavaScript Cookie v2.1.3 + * https://github.com/js-cookie/js-cookie + * + * Copyright 2006, 2015 Klaus Hartl & Fagner Brack + * Released under the MIT license + */ +;(function (factory) { + var registeredInModuleLoader = false; + if (typeof define === 'function' && define.amd) { + define(factory); + registeredInModuleLoader = true; + } + if (typeof exports === 'object') { + module.exports = factory(); + registeredInModuleLoader = true; + } + if (!registeredInModuleLoader) { + var OldCookies = window.Cookies; + var api = window.Cookies = factory(); + api.noConflict = function () { + window.Cookies = OldCookies; + return api; + }; + } +}(function () { + function extend () { + var i = 0; + var result = {}; + for (; i < arguments.length; i++) { + var attributes = arguments[ i ]; + for (var key in attributes) { + result[key] = attributes[key]; + } + } + return result; + } + + function init (converter) { + function api (key, value, attributes) { + var result; + if (typeof document === 'undefined') { + return; + } + + // Write + + if (arguments.length > 1) { + attributes = extend({ + path: '/' + }, api.defaults, attributes); + + if (typeof attributes.expires === 'number') { + var expires = new Date(); + expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * 864e+5); + attributes.expires = expires; + } + + try { + result = JSON.stringify(value); + if (/^[\{\[]/.test(result)) { + value = result; + } + } catch (e) {} + + if (!converter.write) { + value = encodeURIComponent(String(value)) + .replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent); + } else { + value = converter.write(value, key); + } + + key = encodeURIComponent(String(key)); + key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent); + key = key.replace(/[\(\)]/g, escape); + + return (document.cookie = [ + key, '=', value, + attributes.expires ? '; expires=' + attributes.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE + attributes.path ? '; path=' + attributes.path : '', + attributes.domain ? '; domain=' + attributes.domain : '', + attributes.secure ? '; secure' : '' + ].join('')); + } + + // Read + + if (!key) { + result = {}; + } + + // To prevent the for loop in the first place assign an empty array + // in case there are no cookies at all. Also prevents odd result when + // calling "get()" + var cookies = document.cookie ? document.cookie.split('; ') : []; + var rdecode = /(%[0-9A-Z]{2})+/g; + var i = 0; + + for (; i < cookies.length; i++) { + var parts = cookies[i].split('='); + var cookie = parts.slice(1).join('='); + + if (cookie.charAt(0) === '"') { + cookie = cookie.slice(1, -1); + } + + try { + var name = parts[0].replace(rdecode, decodeURIComponent); + cookie = converter.read ? + converter.read(cookie, name) : converter(cookie, name) || + cookie.replace(rdecode, decodeURIComponent); + + if (this.json) { + try { + cookie = JSON.parse(cookie); + } catch (e) {} + } + + if (key === name) { + result = cookie; + break; + } + + if (!key) { + result[name] = cookie; + } + } catch (e) {} + } + + return result; + } + + api.set = api; + api.get = function (key) { + return api.call(api, key); + }; + api.getJSON = function () { + return api.apply({ + json: true + }, [].slice.call(arguments)); + }; + api.defaults = {}; + + api.remove = function (key, attributes) { + api(key, '', extend(attributes, { + expires: -1 + })); + }; + + api.withConverter = init; + + return api; + } + + return init(function () {}); +})); \ No newline at end of file